diff --git a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/SkillDialogSlotFillingTests.cs b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/SkillDialogSlotFillingTests.cs index d1c4cf30ff..6e5d15f3b2 100644 --- a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/SkillDialogSlotFillingTests.cs +++ b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/SkillDialogSlotFillingTests.cs @@ -43,6 +43,22 @@ public void AddSkills() "testSkill/testActionWithSlots", slots)); + // Simple skill, with two actions and multiple slots + var multiParamSlots = new List(); + multiParamSlots.Add(new Slot("param1", new List() { "string" })); + multiParamSlots.Add(new Slot("param2", new List() { "string" })); + multiParamSlots.Add(new Slot("param3", new List() { "string" })); + + var multiActionSkill = ManifestUtilities.CreateSkill( + "testskillwithmultipleactionsandslots", + "testskillwithmultipleactionsandslots", + "https://testskillwithslots.tempuri.org/api/skill", + "testSkill/testAction1", + multiParamSlots); + + multiActionSkill.Actions.Add(ManifestUtilities.CreateAction("testSkill/testAction2", multiParamSlots)); + _skillManifests.Add(multiActionSkill); + // 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) @@ -124,7 +140,34 @@ await this.GetTestFlow(_skillManifests.Single(s => s.Name == "testskillwithslots _mockSkillTransport.VerifyActivityForwardedCorrectly(activity => ValidateActivity(activity, eventToMatch)); } - private bool ValidateActivity(string activitySent, string activityToMatch) + /// + /// Ensure the skillBegin event is sent and includes the slots that were configured in the manifest + /// and present in State. This doesn't pass an action so "global" slot filling is used + /// + /// Task. + [TestMethod] + public async Task SkilllBeginEventNoActionPassed() + { + string eventToMatch = await File.ReadAllTextAsync(@".\TestData\skillBeginEventWithTwoParams.json"); + + var sp = Services.BuildServiceProvider(); + var adapter = sp.GetService(); + + // Data to add to the UserState managed SkillContext made available for slot filling + // within SkillDialog + Dictionary slots = new Dictionary(); + slots.Add("param1", "TEST"); + slots.Add("param2", "TEST2"); + + // Not passing action to test the "global" slot filling behaviour + await this.GetTestFlow(_skillManifests.Single(s => s.Name == "testskillwithmultipleactionsandslots"), null, slots) + .Send("hello") + .StartTestAsync(); + + _mockSkillTransport.VerifyActivityForwardedCorrectly(activity => ValidateActivity(activity, eventToMatch)); + } + + private bool ValidateActivity(string activitySent, string activityToMatch) { return string.Equals(activitySent, activityToMatch); } diff --git a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/Utilities/ManifestUtilities.cs b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/Utilities/ManifestUtilities.cs index ef2044a6bb..f95cead210 100644 --- a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/Utilities/ManifestUtilities.cs +++ b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills.Tests/Utilities/ManifestUtilities.cs @@ -32,5 +32,16 @@ public static SkillManifest CreateSkill(string id, string name, string endpoint, return skillManifest; } + + public static Models.Manifest.Action CreateAction(string id, List slots = null) + { + var action = new Models.Manifest.Action(); + + action.Id = id; + action.Definition = new ActionDefinition(); + action.Definition.Slots = slots; + + return action; + } } } diff --git a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/ActionDefinition.cs b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/ActionDefinition.cs index eeed292cee..4ccbdfbd81 100644 --- a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/ActionDefinition.cs +++ b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/ActionDefinition.cs @@ -12,7 +12,7 @@ public class ActionDefinition public string Description { get; set; } [JsonProperty(PropertyName = "slots")] - public List Slots { get; set; } + public List Slots { get; set; } = new List(); [JsonProperty(PropertyName = "triggers")] public Triggers Triggers { get; set; } diff --git a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/SkillManifest.cs b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/SkillManifest.cs index 9ec0b2f88a..005c56d868 100644 --- a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/SkillManifest.cs +++ b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/Models/Manifest/SkillManifest.cs @@ -32,6 +32,6 @@ public class SkillManifest public AuthenticationConnection[] AuthenticationConnections { get; set; } [JsonProperty(PropertyName = "actions")] - public List Actions { get; set; } + public List Actions { get; set; } = new List(); } } diff --git a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SkillDialog.cs b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SkillDialog.cs index dcc5546192..a68c893560 100644 --- a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SkillDialog.cs +++ b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SkillDialog.cs @@ -91,37 +91,40 @@ public override async Task EndDialogAsync(ITurnContext turnContext, DialogInstan var accessor = _userState.CreateProperty(nameof(SkillContext)); var skillContext = await accessor.GetAsync(innerDc.Context, () => new SkillContext()); + /* In instances where the caller is able to identify/specify the action we process the Action specific slots + In other scenarios (aggregated skill dispatch) we evaluate all possible slots against context and pass across + enabling the Skill to perform it's own action identification. */ + var actionName = options as string; - if (actionName == null) - { - throw new ArgumentException("SkillDialog requires an Action in order to be able to identify which Action within a skill to invoke."); - } - else + if (actionName != null) { - // Find the Action within the selected Skill for slot filling evaluation + // Find the specified within the selected Skill for slot filling evaluation var action = _skillManifest.Actions.SingleOrDefault(a => a.Id == actionName); if (action != null) { // If the action doesn't define any Slots or SkillContext is empty then we skip slot evaluation if (action.Definition.Slots != null && skillContext.Count > 0) { - foreach (Slot slot in action.Definition.Slots) - { - // 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)) - { - slots.Add(slot.Name, slotValue); - - // Send trace to emulator - innerDc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"-->Matched the {slot.Name} slot within SkillContext and passing to the {actionName} action.")).GetAwaiter().GetResult(); - } - } + // Match Slots to Skill Context + slots = await MatchSkillContextToSlots(innerDc, action.Definition.Slots, skillContext); } } 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."); + } + } + else + { + // The caller hasn't got the capability of identifying the action as well as the Skill so we enumerate + // actions and slot data to pass what we have + + // Retrieve a distinct list of all slots, some actions may use the same slot so we use distinct to ensure we only get 1 instance. + var skillSlots = _skillManifest.Actions.SelectMany(s => s.Definition.Slots).Distinct(new SlotEqualityComparer()); + if (skillSlots != null) + { + // Match Slots to Skill Context + slots = await MatchSkillContextToSlots(innerDc, skillSlots.ToList(), skillContext); } } @@ -184,6 +187,34 @@ public override async Task EndDialogAsync(ITurnContext turnContext, DialogInstan return dialogResult; } + /// + /// Map Skill slots to what we have in SkillContext. + /// + /// Dialog Contect. + /// The Slots within an Action. + /// Calling Bot's SkillContext. + /// A filtered SkillContext for the Skill. + private async Task MatchSkillContextToSlots(DialogContext innerDc, List actionSlots, SkillContext skillContext) + { + SkillContext slots = new SkillContext(); + if (actionSlots != null) + { + foreach (Slot slot in actionSlots) + { + // 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)) + { + slots.Add(slot.Name, slotValue); + + // Send trace to emulator + await innerDc.Context.SendActivityAsync(new Activity(type: ActivityTypes.Trace, text: $"-->Matched the {slot.Name} slot within SkillContext and passing to the Skill.")); + } + } + } + + return slots; + } + /// /// Forward an inbound activity on to the Skill. This is a synchronous operation whereby all response activities are aggregated and returned in one batch. /// diff --git a/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SlotEqualityComparer.cs b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SlotEqualityComparer.cs new file mode 100644 index 0000000000..70e4b39139 --- /dev/null +++ b/lib/csharp/microsoft.bot.builder.skills/Microsoft.Bot.Builder.Skills/SlotEqualityComparer.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Bot.Builder.Skills.Models.Manifest; + +namespace Microsoft.Bot.Builder.Skills +{ + public class SlotEqualityComparer : IEqualityComparer + { + public bool Equals(Slot x, Slot y) + { + return x.Name.Equals(y.Name); + } + + public int GetHashCode(Slot obj) + { + return obj.Name.GetHashCode(); + } + } +}