Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Skill Middleware #1104

Merged
merged 4 commits into from
Apr 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ namespace Microsoft.Bot.Builder.Skills.Tests
internal class SkillDialogTest : SkillDialog
{
private MockHttpMessageHandler _mockHttpMessageHandler;

public SkillDialogTest(SkillManifest skillManifest, ResponseManager responseManager, MicrosoftAppCredentialsEx microsoftAppCredentialsEx, IBotTelemetryClient telemetryClient, MockHttpMessageHandler mockHttpMessageHandler, UserState userState) : base(skillManifest, responseManager, microsoftAppCredentialsEx, telemetryClient, userState)
{
_mockHttpMessageHandler = mockHttpMessageHandler;
_httpClient = mockHttpMessageHandler.ToHttpClient();
HttpClient = mockHttpMessageHandler.ToHttpClient();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ namespace Microsoft.Bot.Builder.Skills.Tests
public class SkillDialogTestBase : BotTestBase
{
public IServiceCollection Services { get; set; }

public DialogSet Dialogs { get; set; }

public UserState UserState { get; set; }

public IStatePropertyAccessor<SkillContext> SkillContextAccessor { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Solutions.Testing;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Bot.Builder.Skills.Tests
{
[TestClass]
public class SkillMiddlewareTests
{
private ServiceCollection _serviceCollection;
private UserState _userState;
private IStatePropertyAccessor<SkillContext> _skillContextAccessor;

[TestInitialize]
public void AddSkillManifest()
{
// Initialize service collection
_serviceCollection = new ServiceCollection();

var conversationState = new ConversationState(new MemoryStorage());
_serviceCollection.AddSingleton(conversationState);
_userState = new UserState(new MemoryStorage());
_skillContextAccessor = _userState.CreateProperty<SkillContext>(nameof(SkillContext));
_serviceCollection.AddSingleton(_userState);

_serviceCollection.AddSingleton(sp =>
{
return new BotStateSet(_userState, conversationState);
});

_serviceCollection.AddSingleton<TestAdapter, DefaultTestAdapter>();

}

[TestMethod]
public async Task SkillMiddlewarePopulatesSkillContext()
{
string jsonSkillBeginActivity = await File.ReadAllTextAsync(@".\TestData\skillBeginEvent.json");
var skillBeginEvent = JsonConvert.DeserializeObject<Activity>(jsonSkillBeginActivity);

var skillContextData = new SkillContext();
skillContextData.Add("PARAM1", "TEST1");
skillContextData.Add("PARAM2", "TEST2");

// Ensure we have a copy
skillBeginEvent.Value = new SkillContext(skillContextData);

TestAdapter adapter = new TestAdapter()
.Use(new SkillMiddleware(_userState));

var testFlow = new TestFlow(adapter, async (context, cancellationToken) =>
{
// Validate that SkillContext has been populated by the SKillMiddleware correctly
await ValidateSkillContextData(context, skillContextData);
});

await testFlow.Test(new Activity[] { skillBeginEvent }).StartTestAsync();
}

[TestMethod]
public async Task SkillMiddlewarePopulatesSkillContextDifferentDatatypes()
{
string jsonSkillBeginActivity = await File.ReadAllTextAsync(@".\TestData\skillBeginEvent.json");
var skillBeginEvent = JsonConvert.DeserializeObject<Activity>(jsonSkillBeginActivity);

var skillContextData = new SkillContext();
skillContextData.Add("PARAM1", DateTime.Now);
skillContextData.Add("PARAM2", 3);
skillContextData.Add("PARAM3", null);

// Ensure we have a copy
skillBeginEvent.Value = new SkillContext(skillContextData);

TestAdapter adapter = new TestAdapter()
.Use(new SkillMiddleware(_userState));

var testFlow = new TestFlow(adapter, async (context, cancellationToken) =>
{
// Validate that SkillContext has been populated by the SKillMiddleware correctly
await ValidateSkillContextData(context, skillContextData);
});

await testFlow.Test(new Activity[] { skillBeginEvent }).StartTestAsync();
}

[TestMethod]
public async Task SkillMiddlewareEmptySkillContext()
{
string jsonSkillBeginActivity = await File.ReadAllTextAsync(@".\TestData\skillBeginEvent.json");
var skillBeginEvent = JsonConvert.DeserializeObject<Activity>(jsonSkillBeginActivity);

// Ensure we have a copy
skillBeginEvent.Value = new SkillContext();

TestAdapter adapter = new TestAdapter()
.Use(new SkillMiddleware(_userState));

var testFlow = new TestFlow(adapter, async (context, cancellationToken) =>
{
// Validate that SkillContext has been populated by the SKillMiddleware correctly
await ValidateSkillContextData(context, new SkillContext());
});

await testFlow.Test(new Activity[] { skillBeginEvent }).StartTestAsync();
}

[TestMethod]
public async Task SkillMiddlewareNullSlotData()
{
string jsonSkillBeginActivity = await File.ReadAllTextAsync(@".\TestData\skillBeginEvent.json");
var skillBeginEvent = JsonConvert.DeserializeObject<Activity>(jsonSkillBeginActivity);

skillBeginEvent.Value = null;

TestAdapter adapter = new TestAdapter()
.Use(new SkillMiddleware(_userState));

var testFlow = new TestFlow(adapter, async (context, cancellationToken) =>
{
});

await testFlow.Test(new Activity[] { skillBeginEvent }).StartTestAsync();
}

[TestMethod]
public async Task SkillMiddlewareNullEventName()
{
string jsonSkillBeginActivity = await File.ReadAllTextAsync(@".\TestData\skillBeginEvent.json");
var skillBeginEvent = JsonConvert.DeserializeObject<Activity>(jsonSkillBeginActivity);

skillBeginEvent.Name = null;

TestAdapter adapter = new TestAdapter()
.Use(new SkillMiddleware(_userState));

var testFlow = new TestFlow(adapter, async (context, cancellationToken) =>
{
});

await testFlow.Test(new Activity[] { skillBeginEvent }).StartTestAsync();
}

private async Task ValidateSkillContextData(ITurnContext context, Dictionary<string, object> skillTestDataToValidate)
{
var accessor = _userState.CreateProperty<SkillContext>(nameof(SkillContext));
var skillContext = await _skillContextAccessor.GetAsync(context, () => new SkillContext());

Assert.IsTrue(
skillContext.SequenceEqual(skillTestDataToValidate),
$"SkillContext didn't contain the expected data after Skill middleware processing: {CreateCollectionMismatchMessage(skillContext, skillTestDataToValidate)} ");
}

private string CreateCollectionMismatchMessage (SkillContext context, Dictionary<string, object> test)
{
var contextData = string.Join(",", context.Select(x => x.Key + "=" + x.Value));
var testData = string.Join(",", test.Select(x => x.Key + "=" + x.Value));

return $"Expected: {testData}, Actual: {contextData}";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,15 @@ namespace Microsoft.Bot.Builder.Skills
/// <summary>
/// Context to share state between Bots and Skills.
/// </summary>
public class SkillContext
public class SkillContext : Dictionary<string, object>
{
private readonly Dictionary<string, object> _contextStorage = new Dictionary<string, object>();

public SkillContext()
{
}

public SkillContext(Dictionary<string,object> data)
{
_contextStorage = data;
}

public int Count
{
get { return _contextStorage.Count; }
}

public object this[string name]
{
get
{
return _contextStorage[name];
}

set
{
_contextStorage[name] = value;
}
}

public bool TryGetValue(string key, out object value)
public SkillContext(IDictionary<string, object> collection)
: base(collection)
{
return _contextStorage.TryGetValue(key, out value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace Microsoft.Bot.Builder.Skills
/// </summary>
public class SkillDialog : ComponentDialog
{
protected HttpClient _httpClient = new HttpClient();
private HttpClient _httpClient = new HttpClient();
private readonly MultiProviderAuthDialog _authDialog;
private MicrosoftAppCredentialsEx _microsoftAppCredentialsEx;
private IBotTelemetryClient _telemetryClient;
Expand All @@ -44,7 +44,7 @@ public class SkillDialog : ComponentDialog
/// <param name="proactiveState">Proactive State.</param>
/// <param name="endpointService">Endpoint Service.</param>
/// <param name="telemetryClient">Telemetry Client.</param>
/// <param name="userState">UserState.</param>
/// <param name="userState">User State.</param>
/// <param name="authDialog">Auth Dialog.</param>
public SkillDialog(SkillManifest skillManifest, ResponseManager responseManager, MicrosoftAppCredentialsEx microsoftAppCredentialsEx, IBotTelemetryClient telemetryClient, UserState userState, MultiProviderAuthDialog authDialog = null)
: base(skillManifest.Id)
Expand All @@ -62,6 +62,9 @@ public SkillDialog(SkillManifest skillManifest, ResponseManager responseManager,
}
}

// Protected to enable mocking
protected HttpClient HttpClient { get => _httpClient; set => _httpClient = value; }

/// <summary>
/// When a SkillDialog is started, a skillBegin event is sent which firstly indicates the Skill is being invoked in Skill mode, also slots are also provided where the information exists in the parent Bot.
/// </summary>
Expand All @@ -71,7 +74,7 @@ public SkillDialog(SkillManifest skillManifest, ResponseManager responseManager,
/// <returns>dialog turn result.</returns>
protected override async Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default(CancellationToken))
{
Dictionary<string, object> slotsToPass = new Dictionary<string, object>();
SkillContext slots = new SkillContext();

// Retrieve the SkillContext state object to identify slots (parameters) that can be used to slot-fill when invoking the skill
var accessor = _userState.CreateProperty<SkillContext>(nameof(SkillContext));
Expand All @@ -96,15 +99,15 @@ public SkillDialog(SkillManifest skillManifest, ResponseManager responseManager,
// For each slot we check to see if there is an exact match, if so we pass this slot across to the skill
if (skillContext.TryGetValue(slot.Name, out object slotValue))
{
slotsToPass.Add(slot.Name, slotValue);
slots.Add(slot.Name, slotValue);
}
}
}
}
else
{
// Loosening checks for current Dispatch evaluation, TODO - Review
//throw new ArgumentException($"Passed Action ({actionName}) could not be found within the {_skillManifest.Id} skill manifest action definition.");
// throw new ArgumentException($"Passed Action ({actionName}) could not be found within the {_skillManifest.Id} skill manifest action definition.");
}
}

Expand All @@ -117,7 +120,7 @@ public SkillDialog(SkillManifest skillManifest, ResponseManager responseManager,
recipient: new ChannelAccount(id: activity.Recipient.Id, name: activity.Recipient.Name),
conversation: new ConversationAccount(id: activity.Conversation.Id),
name: SkillEvents.SkillBeginEventName,
value: slotsToPass);
value: slots);

// Send skillBegin event to Skill/Bot
return await ForwardToSkill(innerDc, skillBeginEvent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public enum SkillExceptionType
/// <summary>
/// Other types of exceptions
/// </summary>
Other
Other,
}

public class SkillException : Exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Skills.Models;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;

namespace Microsoft.Bot.Builder.Skills
{
Expand All @@ -22,12 +23,11 @@ public SkillMiddleware(UserState userState)
{
// The skillBegin event signals the start of a skill conversation to a Bot.
var activity = turnContext.Activity;
if (activity != null && activity.Type == ActivityTypes.Event && activity?.Name == SkillEvents.SkillBeginEventName)
if (activity != null && activity.Type == ActivityTypes.Event && activity.Name == SkillEvents.SkillBeginEventName && activity.Value != null)
{
if (activity.Value is Dictionary<string, object> slotData)
var skillContext = activity.Value as SkillContext;
if (skillContext != null)
{
// If we have slotData then we create the SkillContext object within UserState for the skill to access
SkillContext skillContext = new SkillContext(slotData);
var accessor = _userState.CreateProperty<SkillContext>(nameof(SkillContext));
await accessor.SetAsync(turnContext, skillContext);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,17 @@ public DateTimeTestData(string culture)
{
Culture = new CultureInfo(culture);
}

public CultureInfo Culture { get; }

public DateTime InputDateTime { get; set; }

public string ExpectedDateSpeech { get; set; }

public string ExpectedDateSpeechWithSuffix { get; set; }

public string ExpectedTimeSpeech { get; set; }

public string ExpectedTimeSpeechWithSuffix { get; set; }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void ToSpeechString()
private class SomeComplexType
{
public string Number { get; set; }

public object SomeOtherProperty { get; set; }
}
}
Expand Down
Loading