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

Skill support for Slot Filling and Skill/Action wiring for SkillDialog #1098

Merged
merged 4 commits into from
Apr 12, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -89,7 +89,7 @@ public async Task DeserializeValidManifestFile()
[ExpectedException(typeof(JsonSerializationException))]
public async Task DeserializeInvalidManifestFile()
{
using (StreamReader sr = new StreamReader("malformedManifestTemplate.json"))
using (StreamReader sr = new StreamReader(@".\TestData\malformedManifestTemplate.json"))
{
string manifestBody = await sr.ReadToEndAsync();
JsonConvert.DeserializeObject<SkillManifest>(manifestBody);
Expand Down Expand Up @@ -144,7 +144,7 @@ public async Task SkillControllerManifestRequest()
[TestMethod]
public async Task SkillControllerManifestRequestInlineTriggerUtterances()
{
string luisResponse = await File.ReadAllTextAsync("luisCalendarModelResponse.json");
string luisResponse = await File.ReadAllTextAsync(@".\TestData\luisCalendarModelResponse.json");

// Mock the call to LUIS for the model contents
_mockHttp.When("https://westus.api.cognitive.microsoft.com*")
Expand Down Expand Up @@ -195,14 +195,14 @@ public async Task SkillControllerManifestRequestInlineTriggerUtterances()
[TestMethod]
public async Task SkillControllerManifestMissingIntent()
{
string luisResponse = await File.ReadAllTextAsync("luisCalendarModelResponse.json");
string luisResponse = await File.ReadAllTextAsync(@".\TestData\luisCalendarModelResponse.json");

// Mock the call to LUIS for the model contents
_mockHttp.When("https://westus.api.cognitive.microsoft.com*")
.Respond("application/json", luisResponse);

// Pass a manifest that references an intent that does not exist (MISSINGINTENT)
var controller = CreateMockSkillController("manifestInvalidIntent.json");
var controller = CreateMockSkillController(@".\TestData\manifestInvalidIntent.json");

// Replace the NullStream with a MemoryStream
var ms = new MemoryStream();
Expand Down Expand Up @@ -236,14 +236,14 @@ public async Task SkillControllerManifestMissingIntent()
[TestMethod]
public async Task SkillControllerManifestMissingModel()
{
string luisResponse = await File.ReadAllTextAsync("luisCalendarModelResponse.json");
string luisResponse = await File.ReadAllTextAsync(@".\TestData\luisCalendarModelResponse.json");

// Mock the call to LUIS for the model contents
_mockHttp.When("https://westus.api.cognitive.microsoft.com*")
.Respond("application/json", luisResponse);

// Pass a manifest that references an intent that does not exist (MISSINGINTENT)
var controller = CreateMockSkillController("manifestInvalidLUISModel.json");
var controller = CreateMockSkillController(@".\TestData\manifestInvalidLUISModel.json");

// Replace the NullStream with a MemoryStream
var ms = new MemoryStream();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,41 @@
<None Remove="malformedManifestTemplate.json" />
<None Remove="manifestInvalidIntent.json" />
<None Remove="manifestInvalidLUISModel.json" />
<None Remove="manifestTemplate.json" />
<None Remove="skillBeginEvent.json" />
<None Remove="skillBeginEventWithOneParam.json" />
<None Remove="skillBeginEventWithTwoParams.json" />
</ItemGroup>

<ItemGroup>
<Content Include="luisCalendarModelResponse.json">
<Content Include="TestData\luisCalendarModelResponse.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="malformedManifestTemplate.json">
<Content Include="TestData\malformedManifestTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="manifestInvalidIntent.json">
<Content Include="TestData\manifestInvalidIntent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="manifestInvalidLUISModel.json">
<Content Include="TestData\manifestInvalidLUISModel.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="manifestTemplate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\skillBeginEvent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\skillBeginEventWithOneParam.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\skillBeginEventWithTwoParams.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0-preview3.19153.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Bot.Builder.Skills.Auth;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Bot.Builder.Skills.Tests.Mocks
{
public class DummyMicrosoftAppCredentialsEx : MicrosoftAppCredentialsEx
{
public DummyMicrosoftAppCredentialsEx(string appId, string password, string scope) : base(appId, password, scope)
{
}

public override Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Skills.Models.Manifest;
using Microsoft.Bot.Builder.Skills.Tests.Mocks;
using Microsoft.Bot.Builder.Skills.Tests.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RichardSzalay.MockHttp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Bot.Builder.Skills.Tests
{
/// <summary>
/// Test basic invocation of Skills through the SkillDialog.
/// </summary>
[TestClass]
public class SkillDialogInvocationTests : SkillDialogTestBase
{
private SkillManifest _skillManifest;
private MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler();

[TestInitialize]
public void AddSkillManifest()
{
// Simple skill, no slots
_skillManifest = ManifestUtilities.CreateSkill(
"testSkill",
"testSkill",
"https://testskill.tempuri.org/api/skill",
"testSkill/testAction");

// Add the SkillDialog to the available dialogs passing the initialized FakeSkill
Dialogs.Add(new SkillDialogTest(_skillManifest, _skillManifest.Actions[0], null, new DummyMicrosoftAppCredentialsEx(null, null, null), null, _mockHttp, UserState));
}

/// <summary>
/// Create a SkillDialog and send a mesage triggering a HTTP call to the remote skill which the mock intercepts.
/// This ensures the SkillDialog is handling the SkillManifest correctly and sending the HttpRequest to the skill
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task InvokeSkillDialog()
{
// When invoking a Skill the first Activity that is sent is skillBegin so we validate this is sent
// HTTP mock returns "no activities" as per the real scenario and enables the SkillDialog to continue
_mockHttp.When("https://testskill.tempuri.org/api/skill")
.Respond("application/json", "[]");

await this.GetTestFlow(_skillManifest, "testSkill/testAction", null)
.Send("hello")
.StartTestAsync();

try
{
// Check if a request was sent to the mock, if not the test has failed (skill wasn't invoked).
_mockHttp.VerifyNoOutstandingRequest();
}
catch (InvalidOperationException)
{
Assert.Fail("The SkillDialog didn't post an Activity to the HTTP endpoint as expected");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Skills.Auth;
using Microsoft.Bot.Builder.Skills.Models.Manifest;
using Microsoft.Bot.Builder.Skills.Tests.Mocks;
using Microsoft.Bot.Builder.Skills.Tests.Utilities;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RichardSzalay.MockHttp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Bot.Builder.Skills.Tests
{
/// <summary>
/// Test basic invocation of Skills that have slots configured and ensure the slots are filled as expected.
/// </summary>
[TestClass]
public class SkillDialogSlotFillingTests : SkillDialogTestBase
{
private MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler();
private List<SkillManifest> _skillManifests = new List<SkillManifest>();

[TestInitialize]
public void AddSkills()
{
// Simple skill, no slots
_skillManifests.Add(ManifestUtilities.CreateSkill(
"testskill",
"testskill",
"https://testskill.tempuri.org/api/skill",
"testSkill/testAction"));

// Simple skill, with one slot (param1)
var slots = new List<Slot>();
slots.Add(new Slot("param1", new List<string>() { "string" }));
_skillManifests.Add(ManifestUtilities.CreateSkill(
"testskillwithslots",
"testskillwithslots",
"https://testskillwithslots.tempuri.org/api/skill",
"testSkill/testActionWithSlots",
slots));

// Each Skill has a number of actions, these actions are added as their own SkillDialog enabling
// the SkillDialog to know which action is invoked and identify the slots as appropriate.
foreach (var skill in _skillManifests)
{
// Each action within a Skill is registered on it's own as a child of the overall Skill
foreach (var action in skill.Actions)
{
Dialogs.Add(new SkillDialogTest(skill, action, null, new DummyMicrosoftAppCredentialsEx(null, null, null), null, _mockHttp, UserState));
}
}
}

/// <summary>
/// Ensure the SkillBegin event activity is sent to the Skill when starting a skill conversation
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task SkilllBeginEventTest()
{
string eventToMatch = await File.ReadAllTextAsync(@".\TestData\skillBeginEvent.json");

// When invoking a Skill the first Activity that is sent is skillBegin so we validate this is sent
// HTTP mock returns "no activities" as per the real scenario and enables the SkillDialog to continue
_mockHttp.When("https://testskill.tempuri.org/api/skill")
.With(request=> validateActivity(request, eventToMatch))
.Respond("application/json", "[]");

// If the request isn't matched then the event wasn't received as expected
_mockHttp.Fallback.Throw(new InvalidOperationException("Expected Skill Begin event not found"));

var sp = Services.BuildServiceProvider();
var adapter = sp.GetService<TestAdapter>();

await this.GetTestFlow(_skillManifests.Single(s=>s.Name == "testskill"), "testSkill/testAction", null)
.Send("hello")
.StartTestAsync();
}

/// <summary>
/// Ensure the skillBegin event is sent and includes the slots that were configured in the manifest
/// and present in State.
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task SkilllBeginEventWithSlotsTest()
{
string eventToMatch = await File.ReadAllTextAsync(@".\TestData\skillBeginEventWithOneParam.json");

// When invoking a Skill the first Activity that is sent is skillBegin so we validate this is sent
// HTTP mock returns "no activities" as per the real scenario and enables the SkillDialog to continue
_mockHttp.When("https://testskillwithslots.tempuri.org/api/skill")
.With(request => validateActivity(request, eventToMatch))
.Respond("application/json", "[]");

// If the request isn't matched then the event wasn't received as expected
_mockHttp.Fallback.Throw(new InvalidOperationException("Expected Skill Begin event not found"));

var sp = Services.BuildServiceProvider();
var adapter = sp.GetService<TestAdapter>();

// Data to add to the UserState managed SkillContext made available for slot filling
// within SkillDialog
Dictionary<string, object> slots = new Dictionary<string, object>();
slots.Add("param1", "TEST");

await this.GetTestFlow(_skillManifests.Single(s => s.Name == "testskillwithslots"), "testSkill/testActionWithSlots", slots)
.Send("hello")
.StartTestAsync();
}

/// <summary>
/// Ensure the skillBegin event is sent and includes the slots that were configured in the manifest
/// This test has extra data in the SkillContext "memory" which should not be sent across
/// and present in State.
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task SkilllBeginEventWithSlotsTestExtraItems()
{
string eventToMatch = await File.ReadAllTextAsync(@".\TestData\skillBeginEventWithOneParam.json");

// When invoking a Skill the first Activity that is sent is skillBegin so we validate this is sent
// HTTP mock returns "no activities" as per the real scenario and enables the SkillDialog to continue
_mockHttp.When("https://testskillwithslots.tempuri.org/api/skill")
.With(request => validateActivity(request, eventToMatch))
.Respond("application/json", "[]");

// If the request isn't matched then the event wasn't received as expected
_mockHttp.Fallback.Throw(new InvalidOperationException("Expected Skill Begin event not found"));

var sp = Services.BuildServiceProvider();
var adapter = sp.GetService<TestAdapter>();

// Data to add to the UserState managed SkillContext made available for slot filling
// within SkillDialog
Dictionary<string, object> slots = new Dictionary<string, object>();
slots.Add("param1", "TEST");
slots.Add("param2", "TEST");
slots.Add("param3", "TEST");
slots.Add("param4", "TEST");

await this.GetTestFlow(_skillManifests.Single(s => s.Name == "testskillwithslots"), "testSkill/testActionWithSlots", slots)
.Send("hello")
.StartTestAsync();
}

private bool validateActivity(HttpRequestMessage request, string activityToMatch)
{
var activityReceived = request.Content.ReadAsStringAsync().Result;
return string.Equals(activityReceived, activityToMatch);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using Microsoft.Bot.Builder.Skills.Auth;
using Microsoft.Bot.Builder.Skills.Models.Manifest;
using Microsoft.Bot.Builder.Solutions.Responses;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RichardSzalay.MockHttp;

namespace Microsoft.Bot.Builder.Skills.Tests
{
// Extended implementation of SkillDialog for test purposes that enables us to mock the HttpClient
internal class SkillDialogTest : SkillDialog
{
private MockHttpMessageHandler _mockHttpMessageHandler;
public SkillDialogTest(SkillManifest skillManifest, Models.Manifest.Action action, ResponseManager responseManager, MicrosoftAppCredentialsEx microsoftAppCredentialsEx, IBotTelemetryClient telemetryClient, MockHttpMessageHandler mockHttpMessageHandler, UserState userState) : base(skillManifest, action, responseManager, microsoftAppCredentialsEx, telemetryClient, userState)
{
_mockHttpMessageHandler = mockHttpMessageHandler;
_httpClient = mockHttpMessageHandler.ToHttpClient();
}
}
}
Loading