[#5766] Choice options separator should be multi language (#6366)

* Connect LG with ChoiceOptions

* Add unit tests

* Update schemas
This commit is contained in:
Cecilia Avila 2022-06-27 12:12:33 -03:00 коммит произвёл GitHub
Родитель c091272fd3
Коммит 56782c5083
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 620 добавлений и 6 удалений

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

@ -227,7 +227,8 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
var locale = DetermineCulture(dc);
var prompt = await base.OnRenderPromptAsync(dc, state, cancellationToken).ConfigureAwait(false);
var channelId = dc.Context.Activity.ChannelId;
var choiceOptions = ChoiceOptions?.GetValue(dc.State) ?? DefaultChoiceOptions[locale];
var opts = await GetChoiceOptionsAsync(dc, locale).ConfigureAwait(false);
var choiceOptions = opts ?? DefaultChoiceOptions[locale];
var options = dc.State.GetValue<ChoiceInputOptions>(ThisPath.Options);
return AppendChoices(prompt.AsMessageActivity(), channelId, options.Choices, Style.GetValue(dc.State), choiceOptions);
@ -247,6 +248,27 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
}
}
private async Task<ChoiceFactoryOptions> GetChoiceOptionsAsync(DialogContext dc, string locale)
{
if (ChoiceOptions != null)
{
if (ChoiceOptions.ExpressionText != null && ChoiceOptions.ExpressionText.TrimStart().StartsWith("${", StringComparison.InvariantCultureIgnoreCase))
{
// use ITemplate<ChoiceOptionsSet> to bind (aka LG)
return await new ChoiceOptionsSet(ChoiceOptions.ExpressionText).BindAsync(dc).ConfigureAwait(false);
}
else
{
// use Expression to bind
return ChoiceOptions.TryGetValue(dc.State).Value;
}
}
else
{
return DefaultChoiceOptions[locale];
}
}
private string DetermineCulture(DialogContext dc, FindChoicesOptions opt = null)
{
// Note: opt.Locale and Default locale will be considered for deprecation as part of 4.13.

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

@ -0,0 +1,86 @@
// Licensed under the MIT License.
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs.Choices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
{
/// <summary>
/// Sets the ChoiceFactoryOptions.
/// </summary>
public class ChoiceOptionsSet : ChoiceFactoryOptions, ITemplate<ChoiceFactoryOptions>
{
private readonly string template;
/// <summary>
/// Initializes a new instance of the <see cref="ChoiceOptionsSet"/> class.
/// </summary>
public ChoiceOptionsSet()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChoiceOptionsSet"/> class.
/// </summary>
/// <param name="obj">Choice options values.</param>
public ChoiceOptionsSet(object obj)
{
if (obj is string template)
{
this.template = template;
}
}
/// <inheritdoc/>
public async Task<ChoiceFactoryOptions> BindAsync(DialogContext dialogContext, object data = null, CancellationToken cancellationToken = default)
{
if (template == null)
{
return this;
}
var languageGenerator = dialogContext.Services.Get<LanguageGenerator>() ?? throw new MissingMemberException(nameof(LanguageGeneration));
var lgResult = await languageGenerator.GenerateAsync(dialogContext, template, dialogContext.State).ConfigureAwait(false);
if (lgResult is ChoiceFactoryOptions cs)
{
return cs;
}
else if (lgResult is string str)
{
try
{
var jObj = (JArray)JsonConvert.DeserializeObject(str);
var options = new ChoiceFactoryOptions
{
InlineSeparator = jObj[0].ToString(),
InlineOr = jObj[1].ToString(),
InlineOrMore = jObj[2].ToString(),
};
if (jObj.Count > 3)
{
options.IncludeNumbers = jObj[3].ToObject<bool>();
}
return options;
}
catch (JsonReaderException)
{
return null;
}
catch (ArgumentOutOfRangeException)
{
return null;
}
}
return null;
}
}
}

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

@ -169,7 +169,8 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
var channelId = dc.Context.Activity.ChannelId;
var culture = DetermineCulture(dc);
var defaults = DefaultChoiceOptions[culture];
var choiceOptions = ChoiceOptions?.GetValue(dc.State) ?? defaults.Item3;
var opts = await GetChoiceOptionsAsync(dc, defaults).ConfigureAwait(false);
var choiceOptions = opts ?? defaults.Item3;
var confirmChoices = await GetConfirmChoicesAsync(dc, defaults).ConfigureAwait(false);
var prompt = await base.OnRenderPromptAsync(dc, state, cancellationToken).ConfigureAwait(false);
@ -214,5 +215,26 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
return confirmChoices;
}
private async Task<ChoiceFactoryOptions> GetChoiceOptionsAsync(DialogContext dc, (Choice, Choice, ChoiceFactoryOptions) defaults)
{
if (ChoiceOptions != null)
{
if (ChoiceOptions.ExpressionText != null && ChoiceOptions.ExpressionText.TrimStart().StartsWith("${", StringComparison.InvariantCultureIgnoreCase))
{
// use ITemplate<ChoiceOptionsSet> to bind (aka LG)
return await new ChoiceOptionsSet(ChoiceOptions.ExpressionText).BindAsync(dc).ConfigureAwait(false);
}
else
{
// use Expression to bind
return ChoiceOptions.TryGetValue(dc.State).Value;
}
}
else
{
return defaults.Item3;
}
}
}
}

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

@ -170,7 +170,7 @@
}
},
{
"$ref": "schema:#/definitions/equalsExpression"
"$ref": "schema:#/definitions/stringExpression"
}
]
},

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

@ -82,7 +82,7 @@
}
},
{
"$ref": "schema:#/definitions/equalsExpression"
"$ref": "schema:#/definitions/stringExpression"
}
]
},

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

@ -176,6 +176,18 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Tests
await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer);
}
[Fact]
public async Task Action_ChoicesWithChoiceOptions()
{
await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer);
}
[Fact]
public async Task Action_ChoicesWithChoiceOptionsTemplate()
{
await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer);
}
[Fact]
public async Task Action_ConfirmInput()
{
@ -206,6 +218,18 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Tests
await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer);
}
[Fact]
public async Task Action_ConfirmInputWithChoiceOptions()
{
await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer);
}
[Fact]
public async Task Action_ConfirmInputWithChoiceOptionsTemplate()
{
await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer);
}
[Fact]
public async Task Action_DatetimeInput()
{

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

@ -0,0 +1,138 @@
// Licensed under the MIT License.
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Adapters;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Input;
using Microsoft.Bot.Builder.Dialogs.Choices;
using Microsoft.Bot.Schema;
using Moq;
using Xunit;
namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Tests
{
[CollectionDefinition("Dialogs.Adaptive")]
public class ChoiceOptionsSetTests
{
private const string Template = "=${options.OrSeparator()}";
private readonly Activity _activity = MessageFactory.Text("hi");
[Fact]
public void ContructorValidation()
{
Assert.NotNull(new ChoiceOptionsSet());
Assert.NotNull(new ChoiceOptionsSet(Template));
}
[Fact]
public async Task BindAsyncNullTemplate()
{
var choiceOptions = new ChoiceOptionsSet(null);
var context = new TurnContext(new TestAdapter(), _activity);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
var choiceFactory = await choiceOptions.BindAsync(dc).ConfigureAwait(false);
Assert.Equal(choiceOptions, choiceFactory);
}
[Fact]
public async Task BindAsyncNullLanguageGenerator()
{
var choiceOptions = new ChoiceOptionsSet(Template);
var context = new TurnContext(new TestAdapter(), _activity);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
await Assert.ThrowsAsync<MissingMemberException>(async () => await choiceOptions.BindAsync(dc).ConfigureAwait(false));
}
[Fact]
public async Task BindAsyncReturnsLGResult()
{
var choiceOptions = new ChoiceOptionsSet(Template);
var context = new TurnContext(new TestAdapter(), _activity);
var lgResult = new ChoiceFactoryOptions();
var mockLG = new Mock<LanguageGenerator>();
mockLG.Setup(lg => lg.GenerateAsync(It.IsAny<DialogContext>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>())).ReturnsAsync(lgResult);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
dc.Services.Add(mockLG.Object);
var choiceFactory = await choiceOptions.BindAsync(dc).ConfigureAwait(false);
Assert.Equal(lgResult, choiceFactory);
}
[Fact]
public async Task BindAsyncReturnsChoiceFactoryOptions()
{
var choiceOptions = new ChoiceOptionsSet(Template);
var context = new TurnContext(new TestAdapter(), _activity);
var lgResult = "[\",\",\" or\",\", or\",true]";
var mockLG = new Mock<LanguageGenerator>();
mockLG.Setup(lg => lg.GenerateAsync(It.IsAny<DialogContext>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>())).ReturnsAsync(lgResult);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
dc.Services.Add(mockLG.Object);
var choiceFactory = await choiceOptions.BindAsync(dc).ConfigureAwait(false);
Assert.Equal(",", choiceFactory.InlineSeparator);
Assert.Equal(", or", choiceFactory.InlineOrMore);
Assert.Equal(" or", choiceFactory.InlineOr);
Assert.True(choiceFactory.IncludeNumbers);
}
[Fact]
public async Task BindAsyncThrowsArgumentOutOfRangeException()
{
var choiceOptions = new ChoiceOptionsSet(Template);
var context = new TurnContext(new TestAdapter(), _activity);
var lgResult = "[\",\",\" or\"";
var mockLG = new Mock<LanguageGenerator>();
mockLG.Setup(lg => lg.GenerateAsync(It.IsAny<DialogContext>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>())).ReturnsAsync(lgResult);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
dc.Services.Add(mockLG.Object);
Assert.Null(await choiceOptions.BindAsync(dc).ConfigureAwait(false));
}
[Fact]
public async Task BindAsyncThrowsJsonReaderException()
{
var choiceOptions = new ChoiceOptionsSet(Template);
var context = new TurnContext(new TestAdapter(), _activity);
var lgResult = "[\",\",\" or\",\", or\",true]]";
var mockLG = new Mock<LanguageGenerator>();
mockLG.Setup(lg => lg.GenerateAsync(It.IsAny<DialogContext>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>())).ReturnsAsync(lgResult);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
dc.Services.Add(mockLG.Object);
Assert.Null(await choiceOptions.BindAsync(dc).ConfigureAwait(false));
}
[Fact]
public async Task BindAsyncReturnsNull()
{
var choiceOptions = new ChoiceOptionsSet(Template);
var context = new TurnContext(new TestAdapter(), _activity);
var mockLG = new Mock<LanguageGenerator>();
mockLG.Setup(lg => lg.GenerateAsync(It.IsAny<DialogContext>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>())).ReturnsAsync(null);
var dc = new DialogContext(new DialogSet(), context, new DialogState());
dc.Services.Add(mockLG.Object);
Assert.Null(await choiceOptions.BindAsync(dc).ConfigureAwait(false));
}
}
}

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

@ -0,0 +1,140 @@
{
"$schema": "../../../tests.schema",
"$kind": "Microsoft.Test.Script",
"dialog": {
"$kind": "Microsoft.AdaptiveDialog",
"id": "planningTest",
"triggers": [
{
"$kind": "Microsoft.OnBeginDialog",
"actions": [
{
"$kind": "Microsoft.ChoiceInput",
"choices": [
{
"value": "red"
},
{
"value": "green"
},
{
"value": "blue"
}
],
"style": "inline",
"choiceOptions": {
"inlineSeparator": ", ",
"inlineOr": " or2 ",
"inlineOrMore": ", or2 ",
"includeNumbers": true
},
"property": "user.color",
"prompt": "Please select a color:",
"unrecognizedPrompt": "Not a color. Please select a color:"
},
{
"$kind": "Microsoft.SendActivity",
"activity": "${user.color}"
},
{
"$kind": "Microsoft.ChoiceInput",
"choices": [
{
"value": "red"
},
{
"value": "green"
},
{
"value": "blue"
}
],
"style": "inline",
"alwaysPrompt": true,
"choiceOptions": {
"inlineSeparator": ", ",
"inlineOr": " or2 ",
"inlineOrMore": ", or2 ",
"includeNumbers": true
},
"property": "user.color",
"prompt": "Please select a color:",
"unrecognizedPrompt": "Please select a color:"
},
{
"$kind": "Microsoft.SendActivity",
"activity": "${user.color}"
},
{
"$kind": "Microsoft.ChoiceInput",
"choices": [
{
"value": "red"
},
{
"value": "green"
},
{
"value": "blue"
}
],
"style": "inline",
"alwaysPrompt": true,
"choiceOptions": {
"inlineSeparator": ", ",
"inlineOr": " or2 ",
"inlineOrMore": ", or2 ",
"includeNumbers": true
},
"property": "user.color",
"prompt": "Please select a color:",
"unrecognizedPrompt": "Please select a color:"
},
{
"$kind": "Microsoft.SendActivity",
"activity": "${user.color}"
}
]
}
],
"defaultResultProperty": "dialog.result"
},
"script": [
{
"$kind": "Microsoft.Test.UserSays",
"text": "hi"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Please select a color: (1) red, (2) green, or2 (3) blue"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "asdasd"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Not a color. Please select a color: (1) red, (2) green, or2 (3) blue"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "blue"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "blue"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Please select a color: (1) red, (2) green, or2 (3) blue"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "red"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "red"
}
]
}

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

@ -0,0 +1,58 @@
{
"$schema": "../../../tests.schema",
"$kind": "Microsoft.Test.Script",
"dialog": {
"$kind": "Microsoft.AdaptiveDialog",
"id": "planningTest",
"generator": "test.lg",
"triggers": [
{
"$kind": "Microsoft.OnBeginDialog",
"actions": [
{
"$kind": "Microsoft.ChoiceInput",
"alwaysPrompt": true,
"choices": "${MyChoices_simple()}",
"choiceOptions": "${MyChoiceOptions()}",
"style": "inline",
"property": "user.choice",
"prompt": "${MyChoices_Prompt()}",
"unrecognizedPrompt": "${MyChoices_UnknownPrompt()}"
},
{
"$kind": "Microsoft.SendActivity",
"activity": "${user.choice}"
}
]
}
],
"defaultResultProperty": "dialog.result"
},
"locale": "en",
"script": [
{
"$kind": "Microsoft.Test.UserSays",
"text": "hi"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Please select (1) dog or2 (2) cat"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "kitty"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Not a dog or cat. Please select (1) dog or2 (2) cat"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "cat"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "cat"
}
]
}

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

@ -0,0 +1,63 @@
{
"$schema": "../../../tests.schema",
"$kind": "Microsoft.Test.Script",
"dialog": {
"$kind": "Microsoft.AdaptiveDialog",
"id": "planningTest",
"generator": "test.lg",
"triggers": [
{
"$kind": "Microsoft.OnBeginDialog",
"actions": [
{
"$kind": "Microsoft.ConfirmInput",
"style": "inline",
"alwaysPrompt": true,
"choiceOptions": {
"inlineSeparator": ", ",
"inlineOr": " or2 ",
"inlineOrMore": ", or2 ",
"includeNumbers": true
},
"property": "user.confirmed",
"prompt": "${ConfirmInput_Prompt()}",
"unrecognizedPrompt": "${ConfirmInput_UnknownPrompt()}",
"confirmChoices": "${ConfirmInput_simple()}"
},
{
"$kind": "Microsoft.SendActivity",
"activity": "confirmation: ${user.confirmed}"
}
]
}
],
"defaultResultProperty": "dialog.result"
},
"locale": "en",
"script": [
{
"$kind": "Microsoft.Test.UserSays",
"text": "hi"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Please select (1) yep or2 (2) nope"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "asdasd"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Not yep or nope. Please select (1) yep or2 (2) nope"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "yep"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "confirmation: True"
}
]
}

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

@ -0,0 +1,58 @@
{
"$schema": "../../../tests.schema",
"$kind": "Microsoft.Test.Script",
"dialog": {
"$kind": "Microsoft.AdaptiveDialog",
"id": "planningTest",
"generator": "test.lg",
"triggers": [
{
"$kind": "Microsoft.OnBeginDialog",
"actions": [
{
"$kind": "Microsoft.ConfirmInput",
"style": "inline",
"alwaysPrompt": true,
"property": "user.confirmed",
"choiceOptions": "${MyChoiceOptions()}",
"prompt": "${ConfirmInput_Prompt()}",
"unrecognizedPrompt": "${ConfirmInput_UnknownPrompt()}",
"confirmChoices": "${ConfirmInput_simple()}"
},
{
"$kind": "Microsoft.SendActivity",
"activity": "confirmation: ${user.confirmed}"
}
]
}
],
"defaultResultProperty": "dialog.result"
},
"locale": "en",
"script": [
{
"$kind": "Microsoft.Test.UserSays",
"text": "hi"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Please select (1) yep or2 (2) nope"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "asdasd"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "Not yep or nope. Please select (1) yep or2 (2) nope"
},
{
"$kind": "Microsoft.Test.UserSays",
"text": "yep"
},
{
"$kind": "Microsoft.Test.AssertReply",
"text": "confirmation: True"
}
]
}

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

@ -17,6 +17,9 @@
]
```
# MyChoiceOptions
- [", "," or2 ",", or2 ",true]
> CONFIRM INPUT UNIT TEST
# ConfirmInput_Prompt

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

@ -1680,7 +1680,7 @@
}
},
{
"$ref": "#/definitions/equalsExpression"
"$ref": "#/definitions/stringExpression"
}
]
},
@ -2023,7 +2023,7 @@
}
},
{
"$ref": "#/definitions/equalsExpression"
"$ref": "#/definitions/stringExpression"
}
]
},