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

Commit

Permalink
Skill support for Slot Filling and Skill/Action wiring for SkillDialog (
Browse files Browse the repository at this point in the history
#1098)

* Slot Filling implementation and changes to how we wire up Skills+Actions

* tweak to test

* PR comment changes
Moved to one SkillDialog per Skill and use DialogOptions for the ActionId
  • Loading branch information
darrenj authored Apr 12, 2019
1 parent 101279b commit 2a833ff
Show file tree
Hide file tree
Showing 30 changed files with 684 additions and 86 deletions.
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, 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,157 @@
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)
{
Dialogs.Add(new SkillDialogTest(skill, 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, ResponseManager responseManager, MicrosoftAppCredentialsEx microsoftAppCredentialsEx, IBotTelemetryClient telemetryClient, MockHttpMessageHandler mockHttpMessageHandler, UserState userState) : base(skillManifest, responseManager, microsoftAppCredentialsEx, telemetryClient, userState)
{
_mockHttpMessageHandler = mockHttpMessageHandler;
_httpClient = mockHttpMessageHandler.ToHttpClient();
}
}
}
Loading

0 comments on commit 2a833ff

Please sign in to comment.