Updates DialogManager to work with skills (#3709)

* Updated DialogManager to handle skill requests (in adition to parent bot requests).
Added unit tests.
Added end to end Parent->Skill sample with DialogManager for testing.
* Fixed typos, unused namespaces and removed redundant default.
* Added check for active dialog in reprompt and unit test for it.
This commit is contained in:
Gabo Gilabert 2020-04-13 16:21:11 -04:00 коммит произвёл GitHub
Родитель 32107818ed
Коммит f94377b545
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
36 изменённых файлов: 3457 добавлений и 93 удалений

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

@ -0,0 +1,123 @@
// 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.Integration.AspNet.Core.Skills;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.BotBuilderSamples.AdaptiveRootBot.Dialogs;
using Microsoft.BotBuilderSamples.AdaptiveRootBot.Middleware;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot
{
public class AdapterWithErrorHandler : BotFrameworkHttpAdapter
{
private readonly IConfiguration _configuration;
private readonly ConversationState _conversationState;
private readonly ILogger _logger;
private readonly SkillHttpClient _skillClient;
private readonly SkillsConfiguration _skillsConfig;
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger, IStorage storage, UserState userState, ConversationState conversationState, SkillHttpClient skillClient = null, SkillsConfiguration skillsConfig = null)
: base(configuration, logger)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_conversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_skillClient = skillClient;
_skillsConfig = skillsConfig;
this.UseStorage(storage);
this.UseState(userState, conversationState);
OnTurnError = HandleTurnError;
Use(new LoggerMiddleware(logger));
}
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 EndSkillConversationAsync(turnContext);
await ClearConversationStateAsync(turnContext);
}
private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
{
try
{
// Send a message to the user.
var errorMessageText = "The bot 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.
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}");
}
}
private async Task EndSkillConversationAsync(ITurnContext turnContext)
{
if (_skillClient == null || _skillsConfig == null)
{
return;
}
try
{
// Inform the active skill that the conversation is ended so that it has a chance to clean up.
// Note: the root bot manages the ActiveSkillPropertyName, which has a value while the root bot
// has an active conversation with a skill.
var activeSkill = await _conversationState.CreateProperty<BotFrameworkSkill>(MainDialog.ActiveSkillPropertyName).GetAsync(turnContext, () => null);
if (activeSkill != null)
{
var botId = _configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;
var endOfConversation = Activity.CreateEndOfConversationActivity();
endOfConversation.Code = "RootSkillError";
endOfConversation.ApplyConversationReference(turnContext.Activity.GetConversationReference(), true);
await _conversationState.SaveChangesAsync(turnContext, true);
await _skillClient.PostActivityAsync(botId, activeSkill, _skillsConfig.SkillHostEndpoint, (Activity)endOfConversation, CancellationToken.None);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception caught on attempting to send EndOfConversation : {ex}");
}
}
private async Task ClearConversationStateAsync(ITurnContext turnContext)
{
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" for a Web page.
await _conversationState.DeleteAsync(turnContext);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Exception caught on attempting to Delete ConversationState : {ex}");
}
}
}
}

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

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.BotBuilderSamples.AdaptiveRootBot</AssemblyName>
<RootNamespace>Microsoft.BotBuilderSamples.AdaptiveRootBot</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Content Remove="Cards\welcomeCard.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Cards\welcomeCard.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
</ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

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

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Authentication;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot.Authentication
{
/// <summary>
/// Sample claims validator that loads an allowed list from configuration if present
/// and checks that responses are coming from configured skills.
/// </summary>
public class AllowedSkillsClaimsValidator : ClaimsValidator
{
private readonly List<string> _allowedSkills;
public AllowedSkillsClaimsValidator(SkillsConfiguration skillsConfig)
{
if (skillsConfig == null)
{
throw new ArgumentNullException(nameof(skillsConfig));
}
// Load the appIds for the configured skills (we will only allow responses from skills we have configured).
_allowedSkills = (from skill in skillsConfig.Skills.Values select skill.AppId).ToList();
}
public override Task ValidateClaimsAsync(IList<Claim> claims)
{
if (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 (!_allowedSkills.Contains(appId))
{
throw new UnauthorizedAccessException($"Received a request from an application with an appID of \"{appId}\". To enable requests from this skill, add the skill to your configuration file.");
}
}
return Task.CompletedTask;
}
}
}

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

@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot.Bots
{
public class RootBot<T> : ActivityHandler
where T : Dialog
{
private readonly ConversationState _conversationState;
private readonly DialogManager _dialogManager;
public RootBot(ConversationState conversationState, T mainDialog)
{
_conversationState = conversationState;
_dialogManager = new DialogManager(mainDialog);
}
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
if (turnContext.Activity.Type != ActivityTypes.ConversationUpdate)
{
// Run the Dialog with the Activity.
await _dialogManager.OnTurnAsync(turnContext, cancellationToken);
}
else
{
// Let the base class handle the activity.
await base.OnTurnAsync(turnContext, cancellationToken);
}
// Save any state changes that might have occurred during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
// Greet anyone that was not the target (recipient) of this message.
// To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards.
if (member.Id != turnContext.Activity.Recipient.Id)
{
var welcomeCard = CreateAdaptiveCardAttachment();
var activity = MessageFactory.Attachment(welcomeCard);
activity.Speak = "Welcome to the Dialog Skill Prototype!";
await turnContext.SendActivityAsync(activity, cancellationToken);
await _dialogManager.OnTurnAsync(turnContext, cancellationToken);
}
}
}
// Load attachment from embedded resource.
private Attachment CreateAdaptiveCardAttachment()
{
var cardResourcePath = "Microsoft.BotBuilderSamples.AdaptiveRootBot.Cards.welcomeCard.json";
using (var stream = GetType().Assembly.GetManifestResourceStream(cardResourcePath))
{
using (var reader = new StreamReader(stream))
{
var adaptiveCard = reader.ReadToEnd();
return new Attachment
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(adaptiveCard)
};
}
}
}
}
}

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

@ -0,0 +1,29 @@
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "Image",
"url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU",
"size": "stretch"
},
{
"type": "TextBlock",
"spacing": "Medium",
"size": "Medium",
"weight": "Bolder",
"text": "Welcome to the Skill Dialog Sample!",
"wrap": true,
"maxLines": 0,
"color": "Accent"
},
{
"type": "TextBlock",
"size": "default",
"text": "This sample allows you to connect to a skill using a SkillDialog and invoke several actions.",
"wrap": true,
"maxLines": 0
}
]
}

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

@ -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.AdaptiveRootBot.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(BotFrameworkHttpAdapter 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,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.Skills;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot.Controllers
{
/// <summary>
/// A controller that handles skill replies to the bot.
/// This example uses the <see cref="SkillHandler"/> that is registered as a <see cref="ChannelServiceHandler"/> in startup.cs.
/// </summary>
[ApiController]
[Route("api/skills")]
public class SkillController : ChannelServiceController
{
public SkillController(ChannelServiceHandler handler)
: base(handler)
{
}
}
}

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

@ -0,0 +1,298 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Choices;
using Microsoft.Bot.Builder.Integration.AspNet.Core.Skills;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot.Dialogs
{
/// <summary>
/// The main dialog for this bot. It uses a <see cref="SkillDialog"/> to call skills.
/// </summary>
public class MainDialog : ComponentDialog
{
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 SkillActionMessage = "Message";
private readonly IStatePropertyAccessor<BotFrameworkSkill> _activeSkillProperty;
private readonly string _selectedSkillKey = $"{typeof(MainDialog).FullName}.SelectedSkillKey";
private readonly SkillsConfiguration _skillsConfig;
// Dependency injection uses this constructor to instantiate MainDialog.
public MainDialog(ConversationState conversationState, SkillConversationIdFactoryBase conversationIdFactory, SkillHttpClient skillClient, SkillsConfiguration skillsConfig, IConfiguration configuration)
: base(nameof(MainDialog))
{
var botId = configuration.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;
if (string.IsNullOrWhiteSpace(botId))
{
throw new ArgumentException($"{MicrosoftAppCredentials.MicrosoftAppIdKey} is not in configuration");
}
_skillsConfig = skillsConfig ?? throw new ArgumentNullException(nameof(skillsConfig));
if (skillClient == null)
{
throw new ArgumentNullException(nameof(skillClient));
}
if (conversationState == null)
{
throw new ArgumentNullException(nameof(conversationState));
}
// Use helper method to add SkillDialog instances for the configured skills.
AddSkillDialogs(conversationState, conversationIdFactory, skillClient, skillsConfig, botId);
// Add ChoicePrompt to render available skills.
AddDialog(new ChoicePrompt("SkillPrompt"));
// Add ChoicePrompt to render skill actions.
AddDialog(new ChoicePrompt("SkillActionPrompt", SkillActionPromptValidator));
// Add main waterfall dialog for this bot.
var waterfallSteps = new WaterfallStep[]
{
SelectSkillStepAsync,
SelectSkillActionStepAsync,
CallSkillActionStepAsync,
FinalStepAsync
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
// Create state property to track the active skill.
_activeSkillProperty = conversationState.CreateProperty<BotFrameworkSkill>(ActiveSkillPropertyName);
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
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.
var activeSkill = await _activeSkillProperty.GetAsync(innerDc.Context, () => null, cancellationToken);
var activity = innerDc.Context.Activity;
if (activeSkill != null && activity.Type == ActivityTypes.Message && activity.Text.Equals("abort", StringComparison.CurrentCultureIgnoreCase))
{
// Cancel all dialogs when the user says abort.
// The SkillDialog automatically sends an EndOfConversation message to the skill to let the
// skill know that it needs to end its current dialogs, too.
await innerDc.CancelAllDialogsAsync(cancellationToken);
return await innerDc.ReplaceDialogAsync(InitialDialogId, "Canceled! \n\n What skill would you like to call?", cancellationToken);
}
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}
// Render a prompt to select the skill to call.
private async Task<DialogTurnResult> SelectSkillStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Create the PromptOptions from the skill configuration which contain the list of configured skills.
var messageText = stepContext.Options?.ToString() ?? "What skill would you like to call?";
var repromptMessageText = "That was not a valid choice, please select a valid skill.";
var options = new PromptOptions
{
Prompt = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput),
RetryPrompt = MessageFactory.Text(repromptMessageText, repromptMessageText, InputHints.ExpectingInput),
Choices = _skillsConfig.Skills.Select(skill => new Choice(skill.Value.Id)).ToList()
};
// Prompt the user to select a skill.
return await stepContext.PromptAsync("SkillPrompt", options, cancellationToken);
}
// Render a prompt to select the 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;
// 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 options = new PromptOptions
{
Prompt = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput),
Choices = GetSkillActions(selectedSkill)
};
// Prompt the user to select a skill action.
return await stepContext.PromptAsync("SkillActionPrompt", options, cancellationToken);
}
// This validator defaults to Message if the user doesn't select an existing option.
private Task<bool> SkillActionPromptValidator(PromptValidatorContext<FoundChoice> promptContext, CancellationToken cancellationToken)
{
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 };
}
return Task.FromResult(true);
}
// Starts the SkillDialog based on the user's selections.
private async Task<DialogTurnResult> CallSkillActionStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var selectedSkill = (BotFrameworkSkill)stepContext.Values[_selectedSkillKey];
Activity skillActivity;
switch (selectedSkill.Id)
{
case "DialogSkillBot":
skillActivity = CreateDialogSkillBotActivity(((FoundChoice)stepContext.Result).Value, stepContext.Context);
break;
// We can add other case statements here if we support more than one skill.
default:
throw new Exception($"Unknown target skill id: {selectedSkill.Id}.");
}
// Create the BeginSkillDialogOptions and assign the activity to send.
var skillDialogArgs = new BeginSkillDialogOptions { Activity = skillActivity };
// Save active skill in state.
await _activeSkillProperty.SetAsync(stepContext.Context, selectedSkill, cancellationToken);
// Start the skillDialog instance with the arguments.
return await stepContext.BeginDialogAsync(selectedSkill.Id, skillDialogArgs, cancellationToken);
}
// The SkillDialog has ended, render the results (if any) and restart MainDialog.
private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var activeSkill = await _activeSkillProperty.GetAsync(stepContext.Context, () => null, cancellationToken);
// Check if the skill returned any results and display them.
if (stepContext.Result != null)
{
var message = $"Skill \"{activeSkill.Id}\" invocation complete.";
message += $" Result: {JsonConvert.SerializeObject(stepContext.Result)}";
await stepContext.Context.SendActivityAsync(MessageFactory.Text(message, message, inputHint: InputHints.IgnoringInput), cancellationToken: cancellationToken);
}
// Clear the skill selected by the user.
stepContext.Values[_selectedSkillKey] = null;
// Clear active skill in state.
await _activeSkillProperty.DeleteAsync(stepContext.Context, cancellationToken);
// Restart the main dialog with a different message the second time around.
return await stepContext.ReplaceDialogAsync(InitialDialogId, $"Done with \"{activeSkill.Id}\". \n\n What skill would you like to call?", cancellationToken);
}
// Helper method that creates and adds SkillDialog instances for the configured skills.
private void AddSkillDialogs(ConversationState conversationState, SkillConversationIdFactoryBase conversationIdFactory, SkillHttpClient skillClient, SkillsConfiguration skillsConfig, string botId)
{
foreach (var skillInfo in _skillsConfig.Skills.Values)
{
// Create the dialog options.
var skillDialogOptions = new SkillDialogOptions
{
BotId = botId,
ConversationIdFactory = conversationIdFactory,
SkillClient = skillClient,
SkillHostEndpoint = skillsConfig.SkillHostEndpoint,
ConversationState = conversationState,
Skill = skillInfo
};
// Add a SkillDialog for the selected skill.
AddDialog(new SkillDialog(skillDialogOptions, skillInfo.Id));
}
}
// 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 "DialogSkillBot":
choices.Add(new Choice(SkillActionBookFlight));
choices.Add(new Choice(SkillActionBookFlightWithInputParameters));
choices.Add(new Choice(SkillActionGetWeather));
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)
{
// 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))
{
// Note message activities also support input parameters but we are not using them in this example.
return 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;
}
if (activity == null)
{
throw new Exception($"Unable to create dialogArgs for \"{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.
// Note: this is not necessary if we are just forwarding the current activity from context.
activity.ChannelData = turnContext.Activity.ChannelData;
activity.Properties = turnContext.Activity.Properties;
return activity;
}
}
}

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

@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Logging;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot.Middleware
{
/// <summary>
/// Uses an ILogger instance to log user and bot messages. It filters out ContinueConversation events coming from skill responses.
/// </summary>
public class LoggerMiddleware : IMiddleware
{
private readonly ILogger<BotFrameworkHttpAdapter> _logger;
public LoggerMiddleware(ILogger<BotFrameworkHttpAdapter> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
{
// Note: skill responses will show as ContinueConversation events; we don't log those.
// We only log incoming messages from users.
if (turnContext.Activity.Type != ActivityTypes.Event && turnContext.Activity.Name != "ContinueConversation")
{
var message = $"User said: {turnContext.Activity.Text} Type: \"{turnContext.Activity.Type}\" Name: \"{turnContext.Activity.Name}\"";
_logger.LogInformation(message);
}
// Register outgoing handler.
turnContext.OnSendActivities(OutgoingHandler);
// Continue processing messages.
await next(cancellationToken);
}
private async Task<ResourceResponse[]> OutgoingHandler(ITurnContext turnContext, List<Activity> activities, Func<Task<ResourceResponse[]>> next)
{
foreach (var activity in activities)
{
var message = $"Bot said: {activity.Text} Type: \"{activity.Type}\" Name: \"{activity.Name}\"";
_logger.LogInformation(message);
}
return await next();
}
}
}

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

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot
{
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.UseStartup<Startup>();
});
}
}

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

@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Skills;
using Newtonsoft.Json.Linq;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot
{
/// <summary>
/// A <see cref="SkillConversationIdFactory"/> that uses <see cref="IStorage"/> to store
/// and retrieve <see cref="SkillConversationReference"/> instances.
/// </summary>
public class SkillConversationIdFactory : SkillConversationIdFactoryBase
{
private readonly IStorage _storage;
public SkillConversationIdFactory(IStorage storage)
{
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
}
public override async Task<string> CreateSkillConversationIdAsync(SkillConversationIdFactoryOptions options, CancellationToken cancellationToken)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
// Create the storage key based on the SkillConversationIdFactoryOptions.
var conversationReference = options.Activity.GetConversationReference();
var skillConversationId = $"{conversationReference.Conversation.Id}-{options.BotFrameworkSkill.Id}-{conversationReference.ChannelId}-skillconvo";
// Create the SkillConversationReference instance.
var skillConversationReference = new SkillConversationReference
{
ConversationReference = conversationReference,
OAuthScope = options.FromBotOAuthScope
};
// Store the SkillConversationReference using the skillConversationId as a key.
var skillConversationInfo = new Dictionary<string, object> { { skillConversationId, JObject.FromObject(skillConversationReference) } };
await _storage.WriteAsync(skillConversationInfo, cancellationToken).ConfigureAwait(false);
// Return the generated skillConversationId (that will be also used as the conversation ID to call the skill).
return skillConversationId;
}
public override async Task<SkillConversationReference> GetSkillConversationReferenceAsync(string skillConversationId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(skillConversationId))
{
throw new ArgumentNullException(nameof(skillConversationId));
}
// Get the SkillConversationReference from storage for the given skillConversationId.
var skillConversationInfo = await _storage.ReadAsync(new[] { skillConversationId }, cancellationToken).ConfigureAwait(false);
if (skillConversationInfo.Any())
{
var conversationInfo = ((JObject)skillConversationInfo[skillConversationId]).ToObject<SkillConversationReference>();
return conversationInfo;
}
return null;
}
public override async Task DeleteConversationReferenceAsync(string skillConversationId, CancellationToken cancellationToken)
{
// Delete the SkillConversationReference from storage.
await _storage.DeleteAsync(new[] { skillConversationId }, cancellationToken).ConfigureAwait(false);
}
}
}

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

@ -0,0 +1,39 @@
// 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.Extensions.Configuration;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot
{
/// <summary>
/// A helper class that loads Skills information from configuration.
/// </summary>
public class SkillsConfiguration
{
public SkillsConfiguration(IConfiguration configuration)
{
var section = configuration?.GetSection("BotFrameworkSkills");
var skills = section?.Get<BotFrameworkSkill[]>();
if (skills != null)
{
foreach (var skill in skills)
{
Skills.Add(skill.Id, skill);
}
}
var skillHostEndpoint = configuration?.GetValue<string>(nameof(SkillHostEndpoint));
if (!string.IsNullOrWhiteSpace(skillHostEndpoint))
{
SkillHostEndpoint = new Uri(skillHostEndpoint);
}
}
public Uri SkillHostEndpoint { get; }
public Dictionary<string, BotFrameworkSkill> Skills { get; } = new Dictionary<string, BotFrameworkSkill>();
}
}

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

@ -0,0 +1,98 @@
// 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.Builder.Integration.AspNet.Core.Skills;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.BotBuilderSamples.AdaptiveRootBot.Authentication;
using Microsoft.BotBuilderSamples.AdaptiveRootBot.Bots;
using Microsoft.BotBuilderSamples.AdaptiveRootBot.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.BotBuilderSamples.AdaptiveRootBot
{
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();
// Register credential provider.
services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
// Register the skills configuration class.
services.AddSingleton<SkillsConfiguration>();
// Register AuthConfiguration to enable custom claim validation.
services.AddSingleton(sp => new AuthenticationConfiguration { ClaimsValidator = new AllowedSkillsClaimsValidator(sp.GetService<SkillsConfiguration>()) });
// Register the Bot Framework Adapter with error handling enabled.
// Note: some classes expect a BotAdapter and some expect a BotFrameworkHttpAdapter, so
// register the same adapter instance for both types.
services.AddSingleton<BotFrameworkHttpAdapter, AdapterWithErrorHandler>();
services.AddSingleton<BotAdapter>(sp => sp.GetService<BotFrameworkHttpAdapter>());
// Register the skills conversation ID factory, the client and the request handler.
services.AddSingleton<SkillConversationIdFactoryBase, SkillConversationIdFactory>();
services.AddHttpClient<SkillHttpClient>();
services.AddSingleton<ChannelServiceHandler, SkillHandler>();
// Register the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
services.AddSingleton<IStorage, MemoryStorage>();
// Register User state (used by the Dialog system itself).
services.AddSingleton<UserState>();
// Register Conversation state (used by the Dialog system itself).
services.AddSingleton<ConversationState>();
// Register the MainDialog that will be run by the bot.
services.AddSingleton<MainDialog>();
// Register the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddSingleton<IBot, RootBot<MainDialog>>();
}
// 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();
app.UseStaticFiles();
// Uncomment this to support HTTPS.
// app.UseHttpsRedirection();
app.UseRouting();
app.UseWebSockets();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

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

@ -0,0 +1,26 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"MicrosoftAppId": "TODO: Add here the App ID for the bot",
"MicrosoftAppPassword": "TODO: Add here the password for the bot",
"SkillHostEndpoint": "http://localhost:3978/api/skills/",
"BotFrameworkSkills": [
{
"Id": "EchoSkillBot",
"AppId": "TODO: Add here the App ID for the skill",
"SkillEndpoint": "http://localhost:39793/api/messages"
},
{
"Id": "DialogSkillBot",
"AppId": "TODO: Add here the App ID for the skill",
"SkillEndpoint": "http://localhost:39783/api/messages"
}
]
}

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

@ -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>DialogRootBot</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">DialogRootBot Bot</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:3978/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,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Microsoft.BotBuilderSamples.AdaptiveSkillBot</AssemblyName>
<RootNamespace>Microsoft.BotBuilderSamples.AdaptiveSkillBot</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
</ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

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

@ -0,0 +1,179 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
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
{
/// <summary>
/// A root dialog that can route activities sent to the skill to different dialogs.
/// </summary>
public class ActivityRouterDialog : ComponentDialog
{
private readonly DialogSkillBotRecognizer _luisRecognizer;
public ActivityRouterDialog(DialogSkillBotRecognizer luisRecognizer, IConfiguration configuration)
: base(nameof(ActivityRouterDialog))
{
_luisRecognizer = luisRecognizer;
AddDialog(new BookingDialog());
AddDialog(new OAuthTestDialog(configuration));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { ProcessActivityAsync }));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> ProcessActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// A skill can send trace activities if needed :)
await stepContext.Context.TraceActivityAsync($"{GetType().Name}.ProcessActivityAsync()", label: $"Got ActivityType: {stepContext.Context.Activity.Type}", cancellationToken: cancellationToken);
switch (stepContext.Context.Activity.Type)
{
case ActivityTypes.Message:
return await OnMessageActivityAsync(stepContext, cancellationToken);
case ActivityTypes.Invoke:
return await OnInvokeActivityAsync(stepContext, cancellationToken);
case ActivityTypes.Event:
return await OnEventActivityAsync(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);
return new DialogTurnResult(DialogTurnStatus.Complete);
}
}
// This method performs different tasks based on the event name.
private async Task<DialogTurnResult> OnEventActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var activity = stepContext.Context.Activity;
await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnEventActivityAsync()", label: $"Name: {activity.Name}. Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);
// Resolve what to execute based on the event name.
switch (activity.Name)
{
case "BookFlight":
var bookingDetails = new BookingDetails();
if (activity.Value != null)
{
bookingDetails = JsonConvert.DeserializeObject<BookingDetails>(JsonConvert.SerializeObject(activity.Value));
}
// Start the booking dialog
var bookingDialog = FindDialog(nameof(BookingDialog));
return await stepContext.BeginDialogAsync(bookingDialog.Id, bookingDetails, cancellationToken);
case "OAuthTest":
// Start the OAuthTestDialog
var oAuthDialog = FindDialog(nameof(OAuthTestDialog));
return await stepContext.BeginDialogAsync(oAuthDialog.Id, null, cancellationToken);
default:
// We didn't get an event name we can handle.
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized EventName: \"{activity.Name}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
return new DialogTurnResult(DialogTurnStatus.Complete);
}
}
// This method responds right away using an invokeResponse based on the activity name property.
private async Task<DialogTurnResult> OnInvokeActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var activity = stepContext.Context.Activity;
await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnInvokeActivityAsync()", label: $"Name: {activity.Name}. Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);
// Resolve what to execute based on the invoke name.
switch (activity.Name)
{
case "GetWeather":
var location = new Location();
if (activity.Value != null)
{
location = JsonConvert.DeserializeObject<Location>(JsonConvert.SerializeObject(activity.Value));
}
var lookingIntoItMessage = "Getting your weather forecast...";
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"{lookingIntoItMessage} \n\nValue parameters: {JsonConvert.SerializeObject(location)}", lookingIntoItMessage, inputHint: InputHints.IgnoringInput), cancellationToken);
// Create and return an invoke activity with the weather results.
var invokeResponseActivity = new Activity(type: "invokeResponse")
{
Value = new InvokeResponse
{
Body = new[]
{
"New York, NY, Clear, 56 F",
"Bellevue, WA, Mostly Cloudy, 48 F"
},
Status = (int)HttpStatusCode.OK
}
};
await stepContext.Context.SendActivityAsync(invokeResponseActivity, cancellationToken);
break;
default:
// We didn't get an invoke name we can handle.
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized InvokeName: \"{activity.Name}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
break;
}
return new DialogTurnResult(DialogTurnStatus.Complete);
}
// This method just gets a message activity and runs it through LUIS.
// A developer can chose to start a dialog based on the LUIS results (not implemented here).
private async Task<DialogTurnResult> OnMessageActivityAsync(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);
if (!_luisRecognizer.IsConfigured)
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' to the appsettings.json file.", inputHint: InputHints.IgnoringInput), cancellationToken);
}
else
{
// Call LUIS with the utterance.
var luisResult = await _luisRecognizer.RecognizeAsync(stepContext.Context, cancellationToken);
// Create a message showing the LUIS results.
var sb = new StringBuilder();
sb.AppendLine($"LUIS results for \"{activity.Text}\":");
var (intent, intentScore) = luisResult.Intents.FirstOrDefault(x => x.Value.Equals(luisResult.Intents.Values.Max()));
sb.AppendLine($"Intent: \"{intent}\" Score: {intentScore.Score}");
sb.AppendLine($"Entities found: {luisResult.Entities.Count - 1}");
foreach (var luisResultEntity in luisResult.Entities)
{
if (!luisResultEntity.Key.Equals("$instance"))
{
sb.AppendLine($"* {luisResultEntity.Key}");
}
}
await stepContext.Context.SendActivityAsync(MessageFactory.Text(sb.ToString(), inputHint: InputHints.IgnoringInput), cancellationToken);
}
return new DialogTurnResult(DialogTurnStatus.Complete);
}
private string GetObjectAsJsonString(object value)
{
return value == null ? string.Empty : JsonConvert.SerializeObject(value);
}
}
}

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

@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Bots
{
public class SkillBot<T> : IBot
where T : Dialog
{
private readonly ConversationState _conversationState;
private readonly DialogManager _dialogManager;
public SkillBot(ConversationState conversationState, T mainDialog)
{
_conversationState = conversationState;
_dialogManager = new DialogManager(mainDialog);
}
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
await _dialogManager.OnTurnAsync(turnContext, cancellationToken);
// Save any state changes that might have occured during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}
}
}

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

@ -0,0 +1,339 @@
{
"luis_schema_version": "3.2.0",
"versionId": "0.1",
"name": "FlightBooking",
"desc": "Luis Model for CoreBot",
"culture": "en-us",
"tokenizerVersion": "1.0.0",
"intents": [
{
"name": "BookFlight"
},
{
"name": "Cancel"
},
{
"name": "GetWeather"
},
{
"name": "None"
}
],
"entities": [],
"composites": [
{
"name": "From",
"children": [
"Airport"
],
"roles": []
},
{
"name": "To",
"children": [
"Airport"
],
"roles": []
}
],
"closedLists": [
{
"name": "Airport",
"subLists": [
{
"canonicalForm": "Paris",
"list": [
"paris",
"cdg"
]
},
{
"canonicalForm": "London",
"list": [
"london",
"lhr"
]
},
{
"canonicalForm": "Berlin",
"list": [
"berlin",
"txl"
]
},
{
"canonicalForm": "New York",
"list": [
"new york",
"jfk"
]
},
{
"canonicalForm": "Seattle",
"list": [
"seattle",
"sea"
]
}
],
"roles": []
}
],
"patternAnyEntities": [],
"regex_entities": [],
"prebuiltEntities": [
{
"name": "datetimeV2",
"roles": []
}
],
"model_features": [],
"regex_features": [],
"patterns": [],
"utterances": [
{
"text": "book a flight",
"intent": "BookFlight",
"entities": []
},
{
"text": "book a flight from new york",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 19,
"endPos": 26
}
]
},
{
"text": "book a flight from seattle",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 19,
"endPos": 25
}
]
},
{
"text": "book a hotel in new york",
"intent": "None",
"entities": []
},
{
"text": "book a restaurant",
"intent": "None",
"entities": []
},
{
"text": "book flight from london to paris on feb 14th",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 17,
"endPos": 22
},
{
"entity": "To",
"startPos": 27,
"endPos": 31
}
]
},
{
"text": "book flight to berlin on feb 14th",
"intent": "BookFlight",
"entities": [
{
"entity": "To",
"startPos": 15,
"endPos": 20
}
]
},
{
"text": "book me a flight from london to paris",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 22,
"endPos": 27
},
{
"entity": "To",
"startPos": 32,
"endPos": 36
}
]
},
{
"text": "bye",
"intent": "Cancel",
"entities": []
},
{
"text": "cancel booking",
"intent": "Cancel",
"entities": []
},
{
"text": "exit",
"intent": "Cancel",
"entities": []
},
{
"text": "find an airport near me",
"intent": "None",
"entities": []
},
{
"text": "flight to paris",
"intent": "BookFlight",
"entities": [
{
"entity": "To",
"startPos": 10,
"endPos": 14
}
]
},
{
"text": "flight to paris from london on feb 14th",
"intent": "BookFlight",
"entities": [
{
"entity": "To",
"startPos": 10,
"endPos": 14
},
{
"entity": "From",
"startPos": 21,
"endPos": 26
}
]
},
{
"text": "fly from berlin to paris on may 5th",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 9,
"endPos": 14
},
{
"entity": "To",
"startPos": 19,
"endPos": 23
}
]
},
{
"text": "go to paris",
"intent": "BookFlight",
"entities": [
{
"entity": "To",
"startPos": 6,
"endPos": 10
}
]
},
{
"text": "going from paris to berlin",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 11,
"endPos": 15
},
{
"entity": "To",
"startPos": 20,
"endPos": 25
}
]
},
{
"text": "i'd like to rent a car",
"intent": "None",
"entities": []
},
{
"text": "ignore",
"intent": "Cancel",
"entities": []
},
{
"text": "travel from new york to paris",
"intent": "BookFlight",
"entities": [
{
"entity": "From",
"startPos": 12,
"endPos": 19
},
{
"entity": "To",
"startPos": 24,
"endPos": 28
}
]
},
{
"text": "travel to new york",
"intent": "BookFlight",
"entities": [
{
"entity": "To",
"startPos": 10,
"endPos": 17
}
]
},
{
"text": "travel to paris",
"intent": "BookFlight",
"entities": [
{
"entity": "To",
"startPos": 10,
"endPos": 14
}
]
},
{
"text": "what's the forecast for this friday?",
"intent": "GetWeather",
"entities": []
},
{
"text": "what's the weather like for tomorrow",
"intent": "GetWeather",
"entities": []
},
{
"text": "what's the weather like in new york",
"intent": "GetWeather",
"entities": []
},
{
"text": "what's the weather like?",
"intent": "GetWeather",
"entities": []
},
{
"text": "winter is coming",
"intent": "None",
"entities": []
}
],
"settings": []
}

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

@ -0,0 +1,34 @@
// 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.AdaptiveSkillBot.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;
}
[HttpGet]
[HttpPost]
public async Task PostAsync()
{
await _adapter.ProcessAsync(Request, Response, _bot);
}
}
}

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

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Newtonsoft.Json;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.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; }
}
}

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

@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs
{
public class BookingDialog : CancelAndHelpDialog
{
private const string DestinationStepMsgText = "Where would you like to travel to?";
private const string OriginStepMsgText = "Where are you traveling from?";
public BookingDialog()
: base(nameof(BookingDialog))
{
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
AddDialog(new DateResolverDialog());
AddDialog(
new WaterfallDialog(
nameof(WaterfallDialog),
new WaterfallStep[] { DestinationStepAsync, OriginStepAsync, TravelDateStepAsync, ConfirmStepAsync, FinalStepAsync }));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private static bool IsAmbiguous(string timex)
{
var timexProperty = new TimexProperty(timex);
return !timexProperty.Types.Contains(Constants.TimexTypes.Definite);
}
private async Task<DialogTurnResult> DestinationStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
if (bookingDetails.Destination == null)
{
var promptMessage = MessageFactory.Text(DestinationStepMsgText, DestinationStepMsgText, InputHints.ExpectingInput);
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
}
return await stepContext.NextAsync(bookingDetails.Destination, cancellationToken);
}
private async Task<DialogTurnResult> OriginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
bookingDetails.Destination = (string)stepContext.Result;
if (bookingDetails.Origin == null)
{
var promptMessage = MessageFactory.Text(OriginStepMsgText, OriginStepMsgText, InputHints.ExpectingInput);
return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
}
return await stepContext.NextAsync(bookingDetails.Origin, cancellationToken);
}
private async Task<DialogTurnResult> TravelDateStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
bookingDetails.Origin = (string)stepContext.Result;
if (bookingDetails.TravelDate == null || IsAmbiguous(bookingDetails.TravelDate))
{
return await stepContext.BeginDialogAsync(nameof(DateResolverDialog), bookingDetails.TravelDate, cancellationToken);
}
return await stepContext.NextAsync(bookingDetails.TravelDate, cancellationToken);
}
private async Task<DialogTurnResult> ConfirmStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
bookingDetails.TravelDate = (string)stepContext.Result;
var messageText = $"Please confirm, I have you traveling to: {bookingDetails.Destination} from: {bookingDetails.Origin} on: {bookingDetails.TravelDate}. Is this correct?";
var promptMessage = MessageFactory.Text(messageText, messageText, InputHints.ExpectingInput);
return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = promptMessage }, cancellationToken);
}
private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if ((bool)stepContext.Result)
{
var bookingDetails = (BookingDetails)stepContext.Options;
return await stepContext.EndDialogAsync(bookingDetails, cancellationToken);
}
return await stepContext.EndDialogAsync(null, cancellationToken);
}
}
}

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

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs
{
public class CancelAndHelpDialog : ComponentDialog
{
private const string HelpMsgText = "Show help here";
private const string CancelMsgText = "Cancelling...";
public CancelAndHelpDialog(string id)
: base(id)
{
}
protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
var result = await InterruptAsync(innerDc, cancellationToken);
if (result != null)
{
return result;
}
return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
if (innerDc.Context.Activity.Type == ActivityTypes.Message)
{
var text = innerDc.Context.Activity.Text.ToLowerInvariant();
switch (text)
{
case "help":
case "?":
var helpMessage = MessageFactory.Text(HelpMsgText, HelpMsgText, InputHints.ExpectingInput);
await innerDc.Context.SendActivityAsync(helpMessage, cancellationToken);
return new DialogTurnResult(DialogTurnStatus.Waiting);
case "cancel":
case "quit":
var cancelMessage = MessageFactory.Text(CancelMsgText, CancelMsgText, InputHints.IgnoringInput);
await innerDc.Context.SendActivityAsync(cancelMessage, cancellationToken);
return await innerDc.CancelAllDialogsAsync(true, cancellationToken: cancellationToken);
}
}
return null;
}
}
}

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

@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;
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.";
public DateResolverDialog(string id = null)
: base(id ?? nameof(DateResolverDialog))
{
AddDialog(new DateTimePrompt(nameof(DateTimePrompt), DateTimePromptValidator));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { InitialStepAsync, FinalStepAsync }));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private static Task<bool> DateTimePromptValidator(PromptValidatorContext<IList<DateTimeResolution>> promptContext, CancellationToken cancellationToken)
{
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.
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.
// 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);
return Task.FromResult(isDefinite);
}
return Task.FromResult(false);
}
private async Task<DialogTurnResult> InitialStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var timex = (string)stepContext.Options;
var promptMessage = MessageFactory.Text(PromptMsgText, PromptMsgText, InputHints.ExpectingInput);
var repromptMessage = MessageFactory.Text(RepromptMsgText, RepromptMsgText, InputHints.ExpectingInput);
if (timex == null)
{
// We were not given any date at all so prompt the user.
return await stepContext.PromptAsync(
nameof(DateTimePrompt),
new PromptOptions
{
Prompt = promptMessage,
RetryPrompt = repromptMessage,
}, cancellationToken);
}
// We have a Date we just need to check it is unambiguous.
var timexProperty = new TimexProperty(timex);
if (!timexProperty.Types.Contains(Constants.TimexTypes.Definite))
{
// This is essentially a "reprompt" of the data we were given up front.
return await stepContext.PromptAsync(
nameof(DateTimePrompt),
new PromptOptions
{
Prompt = repromptMessage,
}, cancellationToken);
}
return await stepContext.NextAsync(new List<DateTimeResolution> { new DateTimeResolution { Timex = timex } }, cancellationToken);
}
private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var timex = ((List<DateTimeResolution>)stepContext.Result)[0].Timex;
return await stepContext.EndDialogAsync(timex, cancellationToken);
}
}
}

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

@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.AI.Luis;
using Microsoft.Extensions.Configuration;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs
{
public class DialogSkillBotRecognizer : IRecognizer
{
private readonly LuisRecognizer _recognizer;
public DialogSkillBotRecognizer(IConfiguration configuration)
{
var luisIsConfigured = !string.IsNullOrEmpty(configuration["LuisAppId"]) && !string.IsNullOrEmpty(configuration["LuisAPIKey"]) && !string.IsNullOrEmpty(configuration["LuisAPIHostName"]);
if (luisIsConfigured)
{
var luisApplication = new LuisApplication(
configuration["LuisAppId"],
configuration["LuisAPIKey"],
"https://" + configuration["LuisAPIHostName"]);
var luisOptions = new LuisRecognizerOptionsV2(luisApplication);
_recognizer = new LuisRecognizer(luisOptions);
}
}
// 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)
=> await _recognizer.RecognizeAsync(turnContext, cancellationToken);
public virtual async Task<T> RecognizeAsync<T>(ITurnContext turnContext, CancellationToken cancellationToken)
where T : IRecognizerConvert, new()
=> await _recognizer.RecognizeAsync<T>(turnContext, cancellationToken);
}
}

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

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Newtonsoft.Json;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.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; }
}
}

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

@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs
{
public class OAuthTestDialog : CancelAndHelpDialog
{
private readonly string _connectionName;
public OAuthTestDialog(IConfiguration configuration)
: base(nameof(OAuthTestDialog))
{
_connectionName = configuration["ConnectionName"];
AddDialog(new OAuthPrompt(
nameof(OAuthPrompt),
new OAuthPromptSettings
{
ConnectionName = _connectionName,
Text = $"Please Sign In to connection: '{_connectionName}'",
Title = "Sign In",
Timeout = 300000 // User has 5 minutes to login (1000 * 60 * 5)
}));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { PromptStepAsync, LoginStepAsync }));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Get the token from the previous step.
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse != null)
{
// Show the token
var loggedInMessage = "You are now logged in.";
await stepContext.Context.SendActivityAsync(MessageFactory.Text(loggedInMessage, loggedInMessage, InputHints.IgnoringInput), cancellationToken);
var showTokenMessage = "Here is your token:";
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"{showTokenMessage} {tokenResponse.Token}", showTokenMessage, InputHints.IgnoringInput), cancellationToken);
// Sign out
var botAdapter = (BotFrameworkAdapter)stepContext.Context.Adapter;
await botAdapter.SignOutUserAsync(stepContext.Context, _connectionName, null, cancellationToken);
var signOutMessage = "I have signed you out.";
await stepContext.Context.SendActivityAsync(MessageFactory.Text(signOutMessage, signOutMessage, inputHint: InputHints.IgnoringInput), cancellationToken);
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
var tryAgainMessage = "Login was not successful please try again.";
await stepContext.Context.SendActivityAsync(MessageFactory.Text(tryAgainMessage, tryAgainMessage, InputHints.IgnoringInput), cancellationToken);
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
}
}

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

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot
{
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.UseStartup<Startup>();
});
}
}

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

@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot
{
public class SkillAdapterWithErrorHandler : BotFrameworkHttpAdapter
{
public SkillAdapterWithErrorHandler(IConfiguration configuration, ILogger<SkillAdapterWithErrorHandler> logger, IStorage storage, UserState userState, ConversationState conversationState)
: base(configuration, logger)
{
this.UseStorage(storage);
this.UseState(userState, conversationState, false);
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);
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 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);
// 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");
};
}
}
}

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

@ -0,0 +1,78 @@
// 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.Integration.AspNet.Core;
using Microsoft.BotBuilderSamples.AdaptiveSkillBot.Bots;
using Microsoft.BotBuilderSamples.AdaptiveSkillBot.Dialogs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.BotBuilderSamples.AdaptiveSkillBot
{
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();
// Create the Bot Framework Adapter with error handling enabled.
services.AddSingleton<IBotFrameworkHttpAdapter, SkillAdapterWithErrorHandler>();
// Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
services.AddSingleton<IStorage, MemoryStorage>();
// Create the Conversation state. (Used by the Dialog system itself.)
services.AddSingleton<ConversationState>();
// Create the User state. (Used by the Dialog system itself.)
services.AddSingleton<UserState>();
// Register LUIS recognizer
services.AddSingleton<DialogSkillBotRecognizer>();
// The Dialog that will be run by the bot.
services.AddSingleton<ActivityRouterDialog>();
// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddTransient<IBot, SkillBot<ActivityRouterDialog>>();
}
// 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();
app.UseStaticFiles();
//app.UseHttpsRedirection(); Enable this to support https
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

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

@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"ConnectionName": "",
"LuisAppId": "",
"LuisAPIKey": "",
"LuisAPIHostName": ""
}

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

@ -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>DialogSkillBot</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">DialogSkillBot Bot</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:3978/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,116 @@
{
"$schema": "https://schemas.botframework.com/schemas/skills/skill-manifest-2.0.0.json",
"$id": "DialogSkillBot",
"name": "Skill bot with dialogs",
"version": "1.0",
"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.",
"license": "",
"iconUrl": "https://dialogskillbot.contoso.com/icon.png",
"tags": [
"sample",
"travel",
"weather",
"luis"
],
"endpoints": [
{
"name": "default",
"protocol": "BotFrameworkV3",
"description": "Default endpoint for the skill",
"endpointUrl": "https://ggdialogskillbot.azurewebsites.net/api/messages",
"msAppId": "f3fe8762-e50c-4688-b202-a040f522d916"
}
],
"activities": {
"bookFlight": {
"description": "Books a flight (multi turn)",
"type": "event",
"name": "BookFlight",
"value": {
"$ref": "#/definitions/bookingInfo"
},
"resultValue": {
"$ref": "#/definitions/bookingInfo"
}
},
"getWeather": {
"description": "Retrieves and returns the weather for the user's location (single turn, invoke)",
"type": "invoke",
"name": "GetWeather",
"value": {
"$ref": "#/definitions/location"
},
"resultValue": {
"$ref": "#/definitions/weatherReport"
}
},
"passthroughMessage": {
"type": "message",
"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": [
"origin"
],
"properties": {
"origin": {
"type": "string",
"description": "this is the origin city for the flight"
},
"destination": {
"type": "string",
"description": "this is the destination city for the flight"
},
"travelDate": {
"type": "string",
"description": "The date for the flight in YYYY-MM-DD format"
}
}
},
"weatherReport": {
"type": "array",
"description": "Array of forecasts for the next week.",
"items": [
{
"type": "string"
}
]
},
"location": {
"type": "object",
"description": "Location metadata",
"properties": {
"latitude": {
"type": "number",
"title": "Latitude"
},
"longitude": {
"type": "number",
"title": "Longitude"
},
"postalCode": {
"type": "string",
"title": "Postal code"
}
}
}
}
}

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

@ -160,6 +160,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DialogSkillBot", "Functiona
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Adaptive.Testing", "libraries\Microsoft.Bot.Builder.Dialogs.Adaptive.Testing\Microsoft.Bot.Builder.Dialogs.Adaptive.Testing.csproj", "{27ADE3CC-43B5-4563-B6D1-4103E8A6ECCD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Adaptive", "Adaptive", "{AE7143FE-C460-449D-AA50-EFA4820EBADE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRootBot", "FunctionalTests\Skills\Adaptive\AdaptiveRootBot\AdaptiveRootBot.csproj", "{1E638E89-43B9-4381-8CC7-B65548DB0070}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveSkillBot", "FunctionalTests\Skills\Adaptive\AdaptiveSkillBot\AdaptiveSkillBot.csproj", "{3EA28B80-440E-4919-9850-31236968BC04}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -632,6 +638,22 @@ Global
{27ADE3CC-43B5-4563-B6D1-4103E8A6ECCD}.Release|Any CPU.Build.0 = Release|Any CPU
{27ADE3CC-43B5-4563-B6D1-4103E8A6ECCD}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{27ADE3CC-43B5-4563-B6D1-4103E8A6ECCD}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Release|Any CPU.Build.0 = Release|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{1E638E89-43B9-4381-8CC7-B65548DB0070}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{3EA28B80-440E-4919-9850-31236968BC04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3EA28B80-440E-4919-9850-31236968BC04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3EA28B80-440E-4919-9850-31236968BC04}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{3EA28B80-440E-4919-9850-31236968BC04}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{3EA28B80-440E-4919-9850-31236968BC04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -702,6 +724,9 @@ Global
{31D0EA80-192C-4645-911D-F0393535B176} = {3E023AB7-FE1F-41B1-9EF4-1550BCE1DC37}
{AB75F219-CFA4-4051-8A11-3EE7A128AF08} = {3E023AB7-FE1F-41B1-9EF4-1550BCE1DC37}
{27ADE3CC-43B5-4563-B6D1-4103E8A6ECCD} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}

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

@ -2,9 +2,13 @@
// Licensed under the MIT License.
using System;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs.Memory;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;
@ -15,23 +19,23 @@ namespace Microsoft.Bot.Builder.Dialogs
/// </summary>
public class DialogManager
{
private const string LASTACCESS = "_lastAccess";
private string rootDialogId;
private string dialogStateProperty;
private const string LastAccess = "_lastAccess";
private string _rootDialogId;
private readonly string _dialogStateProperty;
/// <summary>
/// Initializes a new instance of the <see cref="DialogManager"/> class.
/// </summary>
/// <param name="rootDialog">rootdialog to use.</param>
/// <param name="rootDialog">Root dialog to use.</param>
/// <param name="dialogStateProperty">alternate name for the dialogState property. (Default is "DialogState").</param>
public DialogManager(Dialog rootDialog = null, string dialogStateProperty = null)
{
if (rootDialog != null)
{
this.RootDialog = rootDialog;
RootDialog = rootDialog;
}
this.dialogStateProperty = dialogStateProperty ?? "DialogState";
_dialogStateProperty = dialogStateProperty ?? "DialogState";
}
/// <summary>
@ -56,7 +60,7 @@ namespace Microsoft.Bot.Builder.Dialogs
/// <value>
/// TurnState.
/// </value>
public TurnContextStateCollection TurnState { get; private set; } = new TurnContextStateCollection();
public TurnContextStateCollection TurnState { get; } = new TurnContextStateCollection();
/// <summary>
/// Gets or sets root dialog to use to start conversation.
@ -68,9 +72,9 @@ namespace Microsoft.Bot.Builder.Dialogs
{
get
{
if (this.rootDialogId != null)
if (_rootDialogId != null)
{
return this.Dialogs.Find(this.rootDialogId);
return Dialogs.Find(_rootDialogId);
}
return null;
@ -78,16 +82,16 @@ namespace Microsoft.Bot.Builder.Dialogs
set
{
this.Dialogs = new DialogSet();
Dialogs = new DialogSet();
if (value != null)
{
this.rootDialogId = value.Id;
this.Dialogs.TelemetryClient = value.TelemetryClient;
this.Dialogs.Add(value);
_rootDialogId = value.Id;
Dialogs.TelemetryClient = value.TelemetryClient;
Dialogs.Add(value);
}
else
{
this.rootDialogId = null;
_rootDialogId = null;
}
}
}
@ -119,63 +123,62 @@ namespace Microsoft.Bot.Builder.Dialogs
/// Runs dialog system in the context of an ITurnContext.
/// </summary>
/// <param name="context">turn context.</param>
/// <param name="cancellationToken">cancelation token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>result of the running the logic against the activity.</returns>
public async Task<DialogManagerResult> OnTurnAsync(ITurnContext context, CancellationToken cancellationToken = default(CancellationToken))
public async Task<DialogManagerResult> OnTurnAsync(ITurnContext context, CancellationToken cancellationToken = default)
{
var botStateSet = new BotStateSet();
// preload turnstate with DM turnstate
foreach (var pair in this.TurnState)
// Preload TurnState with DM TurnState.
foreach (var pair in TurnState)
{
context.TurnState.Set(pair.Key, pair.Value);
}
if (this.ConversationState == null)
if (ConversationState == null)
{
this.ConversationState = context.TurnState.Get<ConversationState>() ?? throw new ArgumentNullException(nameof(this.ConversationState));
ConversationState = context.TurnState.Get<ConversationState>() ?? throw new ArgumentNullException(nameof(ConversationState));
}
else
{
context.TurnState.Set(this.ConversationState);
context.TurnState.Set(ConversationState);
}
botStateSet.Add(this.ConversationState);
botStateSet.Add(ConversationState);
if (this.UserState == null)
if (UserState == null)
{
this.UserState = context.TurnState.Get<UserState>();
UserState = context.TurnState.Get<UserState>();
}
if (this.UserState != null)
if (UserState != null)
{
botStateSet.Add(this.UserState);
botStateSet.Add(UserState);
}
// create property accessors
var lastAccessProperty = ConversationState.CreateProperty<DateTime>(LASTACCESS);
var lastAccess = await lastAccessProperty.GetAsync(context, () => DateTime.UtcNow, cancellationToken: cancellationToken).ConfigureAwait(false);
var lastAccessProperty = ConversationState.CreateProperty<DateTime>(LastAccess);
var lastAccess = await lastAccessProperty.GetAsync(context, () => DateTime.UtcNow, cancellationToken).ConfigureAwait(false);
// Check for expired conversation
var now = DateTime.UtcNow;
if (this.ExpireAfter.HasValue && (DateTime.UtcNow - lastAccess) >= TimeSpan.FromMilliseconds((double)this.ExpireAfter))
if (ExpireAfter.HasValue && (DateTime.UtcNow - lastAccess) >= TimeSpan.FromMilliseconds((double)ExpireAfter))
{
// Clear conversation state
await ConversationState.ClearStateAsync(context, cancellationToken: cancellationToken).ConfigureAwait(false);
await ConversationState.ClearStateAsync(context, cancellationToken).ConfigureAwait(false);
}
lastAccess = DateTime.UtcNow;
await lastAccessProperty.SetAsync(context, lastAccess, cancellationToken: cancellationToken).ConfigureAwait(false);
await lastAccessProperty.SetAsync(context, lastAccess, cancellationToken).ConfigureAwait(false);
// get dialog stack
var dialogsProperty = ConversationState.CreateProperty<DialogState>(this.dialogStateProperty);
DialogState dialogState = await dialogsProperty.GetAsync(context, () => new DialogState(), cancellationToken: cancellationToken).ConfigureAwait(false);
var dialogsProperty = ConversationState.CreateProperty<DialogState>(_dialogStateProperty);
var dialogState = await dialogsProperty.GetAsync(context, () => new DialogState(), cancellationToken).ConfigureAwait(false);
// Create DialogContext
var dc = new DialogContext(this.Dialogs, context, dialogState);
var dc = new DialogContext(Dialogs, context, dialogState);
// get the dialogstatemanager configuration
var dialogStateManager = new DialogStateManager(dc, this.StateConfiguration);
// get the DialogStateManager configuration
var dialogStateManager = new DialogStateManager(dc, StateConfiguration);
await dialogStateManager.LoadAllScopesAsync(cancellationToken).ConfigureAwait(false);
dc.Context.TurnState.Add(dialogStateManager);
@ -185,31 +188,25 @@ namespace Microsoft.Bot.Builder.Dialogs
//
// NOTE: We loop around this block because each pass through we either complete the turn and break out of the loop
// or we have had an exception AND there was an OnError action which captured the error. We need to continue the
// turn based on the actions the OnError handler introduced.
while (true)
// turn based on the actions the OnError handler introduced.
var endOfTurn = false;
while (!endOfTurn)
{
try
{
if (dc.ActiveDialog == null)
if (context.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims))
{
// start root dialog
turnResult = await dc.BeginDialogAsync(this.rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
// The bot is running as a skill.
turnResult = await HandleSkillOnTurnAsync(dc, cancellationToken).ConfigureAwait(false);
}
else
{
// Continue execution
// - This will apply any queued up interruptions and execute the current/next step(s).
turnResult = await dc.ContinueDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
if (turnResult.Status == DialogTurnStatus.Empty)
{
// restart root dialog
turnResult = await dc.BeginDialogAsync(this.rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
// The bot is running as root bot.
turnResult = await HandleBotOnTurnAsync(dc, cancellationToken).ConfigureAwait(false);
}
// turn successfully completed, break the loop
break;
endOfTurn = true;
}
catch (Exception err)
{
@ -227,21 +224,129 @@ namespace Microsoft.Bot.Builder.Dialogs
// save all state scopes to their respective botState locations.
await dialogStateManager.SaveAllChangesAsync(cancellationToken).ConfigureAwait(false);
// save botstate changes
// save BotState changes
await botStateSet.SaveAllChangesAsync(dc.Context, false, cancellationToken).ConfigureAwait(false);
return new DialogManagerResult { TurnResult = turnResult };
}
/// <summary>
/// Helper to send a trace activity with a memory snapshot of the active dialog DC.
/// </summary>
private static async Task SendStateSnapshotTraceAsync(DialogContext dc, string traceLabel, CancellationToken cancellationToken)
{
// send trace of memory
var snapshotDc = dc;
while (snapshotDc.Child != null)
var snapshot = GetActiveDialogContext(dc).State.GetMemorySnapshot();
var traceActivity = (Activity)Activity.CreateTraceActivity("BotState", "https://www.botframework.com/schemas/botState", snapshot, traceLabel);
await dc.Context.SendActivityAsync(traceActivity, cancellationToken).ConfigureAwait(false);
}
// We should only cancel the current dialog stack if the EoC activity is coming from a parent (a root bot or another skill).
// When the EoC is coming back from a child, we should just process that EoC normally through the
// dialog stack and let the child dialogs handle that.
private static bool IsEocComingFromParent(ITurnContext turnContext)
{
// To determine the direction we check callerId property which is set to the parent bot
// by the BotFrameworkHttpClient on outgoing requests.
return !string.IsNullOrWhiteSpace(turnContext.Activity.CallerId);
}
// Recursively walk up the DC stack to find the active DC.
private static DialogContext GetActiveDialogContext(DialogContext dialogContext)
{
var child = dialogContext.Child;
if (child == null)
{
snapshotDc = snapshotDc.Child;
return dialogContext;
}
var snapshot = snapshotDc.State.GetMemorySnapshot();
var traceActivity = (Activity)Activity.CreateTraceActivity("BotState", "https://www.botframework.com/schemas/botState", snapshot, "Bot State");
await dc.Context.SendActivityAsync(traceActivity).ConfigureAwait(false);
return GetActiveDialogContext(child);
}
return new DialogManagerResult() { TurnResult = turnResult };
private async Task<DialogTurnResult> HandleSkillOnTurnAsync(DialogContext dc, CancellationToken cancellationToken)
{
// the bot is running as a skill.
var turnContext = dc.Context;
// Process remote cancellation
if (turnContext.Activity.Type == ActivityTypes.EndOfConversation && dc.ActiveDialog != null && IsEocComingFromParent(turnContext))
{
// Handle remote cancellation request from parent.
var activeDialogContext = GetActiveDialogContext(dc);
var remoteCancelText = "Skill was canceled through an EndOfConversation activity from the parent.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{remoteCancelText}", cancellationToken: cancellationToken).ConfigureAwait(false);
// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
return await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false);
}
// Handle reprompt
// Process a reprompt event sent from the parent.
if (turnContext.Activity.Type == ActivityTypes.Event && turnContext.Activity.Name == DialogEvents.RepromptDialog)
{
if (dc.ActiveDialog == null)
{
return new DialogTurnResult(DialogTurnStatus.Empty);
}
await dc.RepromptDialogAsync(cancellationToken).ConfigureAwait(false);
return new DialogTurnResult(DialogTurnStatus.Waiting);
}
// Continue execution
// - This will apply any queued up interruptions and execute the current/next step(s).
var turnResult = await dc.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (turnResult.Status == DialogTurnStatus.Empty)
{
// restart root dialog
var startMessageText = $"Starting {_rootDialogId}.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{startMessageText}", cancellationToken: cancellationToken).ConfigureAwait(false);
turnResult = await dc.BeginDialogAsync(_rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
await SendStateSnapshotTraceAsync(dc, "Skill State", cancellationToken).ConfigureAwait(false);
// Send end of conversation if it is completed or cancelled.
if (turnResult.Status == DialogTurnStatus.Complete || turnResult.Status == DialogTurnStatus.Cancelled)
{
var endMessageText = $"Dialog {_rootDialogId} has **completed**. Sending EndOfConversation.";
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{endMessageText}", value: turnResult.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
// Send End of conversation at the end.
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = turnResult.Result };
await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
}
return turnResult;
}
private async Task<DialogTurnResult> HandleBotOnTurnAsync(DialogContext dc, CancellationToken cancellationToken)
{
DialogTurnResult turnResult;
// the bot is running as a root bot.
if (dc.ActiveDialog == null)
{
// start root dialog
turnResult = await dc.BeginDialogAsync(_rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
else
{
// Continue execution
// - This will apply any queued up interruptions and execute the current/next step(s).
turnResult = await dc.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
if (turnResult.Status == DialogTurnStatus.Empty)
{
// restart root dialog
turnResult = await dc.BeginDialogAsync(_rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
await SendStateSnapshotTraceAsync(dc, "Bot State", cancellationToken).ConfigureAwait(false);
return turnResult;
}
}
}

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

@ -4,15 +4,15 @@
using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs.Adaptive;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Actions;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Conditions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.Bot.Builder.Dialogs.Tests
@ -20,6 +20,15 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
[TestClass]
public class DialogManagerTests
{
// An App ID for a parent bot.
private readonly string _parentBotId = Guid.NewGuid().ToString();
// An App ID for a skill bot.
private readonly string _skillBotId = Guid.NewGuid().ToString();
// Property to capture the DialogManager turn results and do assertions.
private DialogManagerResult _dmTurnResult;
public TestContext TestContext { get; set; }
[TestMethod]
@ -46,7 +55,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
var firstConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
var adaptiveDialog = CreateTestDialog("conversation.name");
await CreateFlow(adaptiveDialog, storage, firstConversationId, dialogStateProperty: "dialogState")
.Send("hi")
@ -65,7 +74,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
var secondConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
var adaptiveDialog = CreateTestDialog("conversation.name");
await CreateFlow(adaptiveDialog, storage, firstConversationId)
.Send("hi")
@ -89,7 +98,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
var secondConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "user.name");
var adaptiveDialog = CreateTestDialog("user.name");
await CreateFlow(adaptiveDialog, storage, firstConversationId)
.Send("hi")
@ -111,7 +120,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
var secondConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var outerAdaptiveDialog = CreateTestDialog(property: "user.name");
var outerAdaptiveDialog = CreateTestDialog("user.name");
var componentDialog = new ComponentDialog();
componentDialog.AddDialog(outerAdaptiveDialog);
@ -186,7 +195,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
}
};
DialogManager dm = new DialogManager(rootDialog);
var dm = new DialogManager(rootDialog);
dm.Dialogs.Add(new SimpleDialog() { Id = "test" });
await new TestFlow(adapter, async (turnContext, cancellationToken) =>
@ -199,12 +208,90 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
.StartTestAsync();
}
private Dialog CreateTestDialog(string property = "user.name")
[TestMethod]
public async Task SkillSendsEoCAndValuesAtDialogEnd()
{
var firstConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true)
.Send("hi")
.AssertReply("Hello, what is your name?")
.Send("Carlos")
.AssertReply("Hello Carlos, nice to meet you!")
.AssertReply(activity =>
{
Assert.AreEqual(activity.Type, ActivityTypes.EndOfConversation);
Assert.AreEqual(((Activity)activity).Value, "Carlos");
})
.StartTestAsync();
Assert.AreEqual(DialogTurnStatus.Complete, _dmTurnResult.TurnResult.Status);
}
[TestMethod]
public async Task SkillHandlesEoCFromParent()
{
var firstConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
var eocActivity = new Activity(ActivityTypes.EndOfConversation) { CallerId = _parentBotId };
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true)
.Send("hi")
.AssertReply("Hello, what is your name?")
.Send(eocActivity)
.StartTestAsync();
Assert.AreEqual(DialogTurnStatus.Cancelled, _dmTurnResult.TurnResult.Status);
}
[TestMethod]
public async Task SkillHandlesRepromptFromParent()
{
var firstConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
var repromptEvent = new Activity(ActivityTypes.Event) { Name = DialogEvents.RepromptDialog };
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true)
.Send("hi")
.AssertReply("Hello, what is your name?")
.Send(repromptEvent)
.AssertReply("Hello, what is your name?")
.StartTestAsync();
Assert.AreEqual(DialogTurnStatus.Waiting, _dmTurnResult.TurnResult.Status);
}
[TestMethod]
public async Task SkillShouldReturnEmptyOnRepromptWithNoDialog()
{
var firstConversationId = Guid.NewGuid().ToString();
var storage = new MemoryStorage();
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
var repromptEvent = new Activity(ActivityTypes.Event) { Name = DialogEvents.RepromptDialog };
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true)
.Send(repromptEvent)
.StartTestAsync();
Assert.AreEqual(DialogTurnStatus.Empty, _dmTurnResult.TurnResult.Status);
}
private Dialog CreateTestDialog(string property)
{
return new AskForNameDialog(property.Replace(".", string.Empty), property);
}
private TestFlow CreateFlow(Dialog dialog, IStorage storage, string conversationId, string dialogStateProperty = null)
private TestFlow CreateFlow(Dialog dialog, IStorage storage, string conversationId, string dialogStateProperty = null, bool isSkillFlow = false)
{
var convoState = new ConversationState(storage);
var userState = new UserState(storage);
@ -215,62 +302,81 @@ namespace Microsoft.Bot.Builder.Dialogs.Tests
.UseState(userState, convoState)
.Use(new TranscriptLoggerMiddleware(new TraceTranscriptLogger(traceActivity: false)));
DialogManager dm = new DialogManager(dialog, dialogStateProperty: dialogStateProperty);
var dm = new DialogManager(dialog, dialogStateProperty: dialogStateProperty);
return new TestFlow(adapter, async (turnContext, cancellationToken) =>
{
await dm.OnTurnAsync(turnContext, cancellationToken: cancellationToken).ConfigureAwait(false);
if (isSkillFlow)
{
// Create a skill ClaimsIdentity and put it in TurnState so SkillValidation.IsSkillClaim() returns true.
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(AuthenticationConstants.VersionClaim, "2.0"));
claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AudienceClaim, _skillBotId));
claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AuthorizedParty, _parentBotId));
turnContext.TurnState.Add(BotAdapter.BotIdentityKey, claimsIdentity);
}
// Capture the last DialogManager turn result for assertions.
_dmTurnResult = await dm.OnTurnAsync(turnContext, cancellationToken: cancellationToken).ConfigureAwait(false);
});
}
public class AskForNameDialog : ComponentDialog, IDialogDependencies
private class AskForNameDialog : ComponentDialog, IDialogDependencies
{
private readonly string _property;
public AskForNameDialog(string id, string property)
: base(id)
{
AddDialog(new TextPrompt("prompt"));
this.Property = property;
_property = property;
}
public string Property { get; set; }
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default)
{
if (outerDc.State.TryGetValue<string>(this.Property, out string result))
if (outerDc.State.TryGetValue<string>(_property, out var result))
{
await outerDc.Context.SendActivityAsync($"Hello {result.ToString()}, nice to meet you!");
return await outerDc.EndDialogAsync(result);
await outerDc.Context.SendActivityAsync($"Hello {result}, nice to meet you!", cancellationToken: cancellationToken);
return await outerDc.EndDialogAsync(result, cancellationToken);
}
return await outerDc.BeginDialogAsync(
"prompt",
new PromptOptions
{
Prompt = new Activity { Type = ActivityTypes.Message, Text = "Hello, what is your name?" },
RetryPrompt = new Activity { Type = ActivityTypes.Message, Text = "Hello, what is your name?" },
},
cancellationToken: cancellationToken)
"prompt",
new PromptOptions
{
Prompt = new Activity
{
Type = ActivityTypes.Message,
Text = "Hello, what is your name?"
},
RetryPrompt = new Activity
{
Type = ActivityTypes.Message,
Text = "Hello, what is your name?"
}
},
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
public IEnumerable<Dialog> GetDependencies()
{
return this.Dialogs.GetDialogs();
return Dialogs.GetDialogs();
}
public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext outerDc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
{
outerDc.State.SetValue(this.Property, result);
await outerDc.Context.SendActivityAsync($"Hello {result.ToString()}, nice to meet you!");
return await outerDc.EndDialogAsync(result);
outerDc.State.SetValue(_property, result);
await outerDc.Context.SendActivityAsync($"Hello {result}, nice to meet you!", cancellationToken: cancellationToken);
return await outerDc.EndDialogAsync(result, cancellationToken);
}
}
public class SimpleDialog : Dialog
private class SimpleDialog : Dialog
{
public async override Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default)
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default)
{
await dc.Context.SendActivityAsync("simple");
return await dc.EndDialogAsync();
await dc.Context.SendActivityAsync("simple", cancellationToken: cancellationToken);
return await dc.EndDialogAsync(cancellationToken: cancellationToken);
}
}
}