Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Bot Framework と Microsoft Graph で DevOps その 19 : LUIS と多言語対応ボットの実装編

Tags: luis await async

前回は Luis 応用編ということでエンティティやフォームフローとの連携を紹介しました。今回は開発中のボットアプリに組み込むところとユニットテストについて見ていきます。

多言語対応

将来的に日本語で datetimeV2 が出れば別ですが、今回は英語版の LUIS を使います。よってユーザーからのメッセージは一旦翻訳しましょう。

翻訳サービス

Cognitive の翻訳サービスを使います。詳細はこちら。ただ、探し方が悪いのか C# SDK がなかった。。

1. Azure ポータルから Translator Text API のキーを取得します。

2. ボットアプリプロジェクトの Services フォルダに ITranslationService.cs を追加し、コードを差し替え。

using System.Threading.Tasks;

namespace O365Bot.Services
{
    public interface ITranslationService
    {
        Task Translate(string content, string from, string to);
    }
}

3. TranslationService.cs を追加し、コードを差し替え。

using System;
using System.Configuration;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace O365Bot.Services
{
    public class TranslationService : ITranslationService
    {
        private const string translateUri = "https://api.microsofttranslator.com/V2/Http.svc/Translate?text=";
        private const string authUri = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken";

        public async Task Translate(string content, string from, string to)
        {
            var token = await GetAccessToken();
            if (string.IsNullOrEmpty(token))
                throw new Exception("failed to auth translate API");

            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                var res = await client.GetAsync($"{translateUri}{Uri.EscapeUriString(content)}&from={from}&to={to}");
                if (res.IsSuccessStatusCode)
                {
                    XDocument xdoc = XDocument.Parse(await res.Content.ReadAsStringAsync());
                    return (xdoc.FirstNode as XElement).Value;
                }
                else
                    return "";
            }
        }

        /// 
        /// アクセストークンの取得
        /// 
        private async Task GetAccessToken()
        {
            using(HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.TryAddWithoutValidation("Ocp-Apim-Subscription-Key", ConfigurationManager.AppSettings["TranslationKey"]);
                var res = await client.PostAsync(authUri, null);
                if (res.IsSuccessStatusCode)
                    return await res.Content.ReadAsStringAsync();
                else
                    return "";
            }
        }        
    }
}

4. Global.asax.cs の Application_Start を以下に差し替え。ITranslationService を追加しています。

protected void Application_Start()
{
    this.RegisterBotModules();
    GlobalConfiguration.Configure(WebApiConfig.Register);

    AuthBot.Models.AuthSettings.Mode = ConfigurationManager.AppSettings["ActiveDirectory.Mode"];
    AuthBot.Models.AuthSettings.EndpointUrl = ConfigurationManager.AppSettings["ActiveDirectory.EndpointUrl"];
    AuthBot.Models.AuthSettings.Tenant = ConfigurationManager.AppSettings["ActiveDirectory.Tenant"];
    AuthBot.Models.AuthSettings.RedirectUrl = ConfigurationManager.AppSettings["ActiveDirectory.RedirectUrl"];
    AuthBot.Models.AuthSettings.ClientId = ConfigurationManager.AppSettings["ActiveDirectory.ClientId"];
    AuthBot.Models.AuthSettings.ClientSecret = ConfigurationManager.AppSettings["ActiveDirectory.ClientSecret"];

    var builder = new ContainerBuilder();
    builder.RegisterType().As();
    builder.RegisterType().As();
    builder.RegisterType().As();

    Container = builder.Build();
}

LUIS ダイアログでの翻訳

LUIS ダイアログに翻訳した文章を渡したい場合、 GetLuisQueryTextAsync をオーバーライドすることで実現できます。LuisRootDiaog.cs に以下のメソッドを追加します。

/// 
/// LUIS にわたるテキストの翻訳
/// 
protected override async Task GetLuisQueryTextAsync(IDialogContext context, IMessageActivity message)
{
    using (var scope = WebApiApplication.Container.BeginLifetimeScope())
    {
        ITranslationService service = scope.Resolve();
        return await service.Translate(message.Text, message.Locale, "en");
    }
}

この場合 LUIS に対する処理だけが翻訳され、元の文章は保持されます。

エンティティの解析と処理フローの検討

LUIS により一部のフィールドはエンティティとして取得が可能となります。そのことを踏まえて処理フローも検討の余地が出てきます。エンティティの解析と、処理フローを一旦以下の様に変更してみました。CreateEventDialog.cs を差し替えます。

using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.Luis.Models;
using Microsoft.Bot.Connector;
using Microsoft.Graph;
using Newtonsoft.Json.Linq;
using O365Bot.Models;
using O365Bot.Resources;
using O365Bot.Services;
using System;
using System.Threading.Tasks;

namespace O365Bot.Dialogs
{
    [Serializable]
    public class CreateEventDialog : IDialog // このダイアログが完了時に返す型
    {
        LuisResult luisResult;
        
        public CreateEventDialog(LuisResult luisResult)
        {
            this.luisResult = luisResult;
        }

        public async Task StartAsync(IDialogContext context)
        {
            var @event = new OutlookEvent();

            // LuisResult の Entities から初期値を設定
            foreach (EntityRecommendation entity in luisResult.Entities)
            {
                switch (entity.Type)
                {
                    case "Calendar.Subject":
                        @event.Subject = entity.Entity;
                        break;
                    // 日付型は様々なパターンで返ってくるので必要に応じて処理
                    case "builtin.datetimeV2.datetimerange":
                    case "builtin.datetimeV2.datetime":
                    case "builtin.datetimeV2.duration":
                    case "builtin.datetimeV2.date":
                    case "builtin.datetimeV2.time":
                        foreach (var vals in entity.Resolution.Values)
                        {
                            switch(((JArray)vals).First.SelectToken("type").ToString())
                            {
                                // 範囲の場合、スタートと期間を処理
                                case "datetimerange":
                                    var start = (DateTime)((JArray)vals).First["start"];
                                    var end = (DateTime)((JArray)vals).Last["end"];
                                    @event.Start = start;
                                    @event.Hours = end.Hour - start.Hour;
                                    break;
                                case "datetime":
                                case "date":
                                    @event.Start = (DateTime)((JArray)vals).Last["value"];
                                    break;
                                case "time":
                                    @event.Start =DateTime.Now.Date.Add(((DateTime)((JArray)vals).Last["value"]).TimeOfDay);
                                    break;
                                // Duration は秒で返ってくる
                                case "duration":
                                    @event.Hours = (int)((JArray)vals).Last["value"] / 60 * 60;
                                    break;
                            }                            
                        }
                        break;
                }
            }

            // 結果から全日イベントか確認
            if (@event.Start.Hour == 0 && @event.Hours == 0)
                @event.IsAllDay = true;
            
            @event.Description = (context.Activity as Activity).Text;

            if (string.IsNullOrEmpty(@event.Subject))
                @event.Subject = (context.Activity as Activity).Text;

            // FormFlow に初期値を渡して実行
            var outlookEventFormDialog = new FormDialog(@event, BuildOutlookEventForm, FormOptions.PromptInStart);
            context.Call(outlookEventFormDialog, this.ResumeAfterDialog);
        }

        private async Task ResumeAfterDialog(IDialogContext context, IAwaitable result)
        {
            await context.PostAsync(O365BotLabel.Event_Created);

            // ダイアログの完了を宣言
            context.Done(true);
        }

        public static IForm BuildOutlookEventForm()
        {
            OnCompletionAsyncDelegate processOutlookEventCreate = async (context, state) =>
            {
                using (var scope = WebApiApplication.Container.BeginLifetimeScope())
                {
                    IEventService service = scope.Resolve(new TypedParameter(typeof(IDialogContext), context));
                    // TimeZone は https://graph.microsoft.com/beta/me/mailboxSettings で取得可能だがここでは一旦ハードコード
                    Event @event = new Event()
                    {
                        Subject = state.Subject,
                        Start = new DateTimeTimeZone() { DateTime = state.Start.ToString(), TimeZone = "Tokyo Standard Time" },
                        IsAllDay = state.IsAllDay,
                        End = state.IsAllDay ? null : new DateTimeTimeZone() { DateTime = state.Start.AddHours(state.Hours).ToString(), TimeZone = "Tokyo Standard Time" },
                        Body = new ItemBody() { Content = state.Description, ContentType = BodyType.Text }
                    };
                    await service.CreateEvent(@event);
                }
            };

            return new FormBuilder()
                .Message(O365BotLabel.Event_Create)
                .Field(nameof(OutlookEvent.Subject))
                .Field(nameof(OutlookEvent.Start), active: (state) =>
                {
                    // Start に値があるかは初期値と比較
                    if (state.Start == default(DateTime))
                        return true;
                    else
                        return false;
                })
                .Field(nameof(OutlookEvent.Hours), active: (state) =>
                {
                    // 期間に値があるかは初期値と比較
                    if (state.Hours == default(double) && !state.IsAllDay)
                        return true;
                    else
                        return false;
                })
                .Field(nameof(OutlookEvent.Description))
                .OnCompletion(processOutlookEventCreate)
                .Build();
        }        
    }
}

不要なファイルの削除

もう RootDialog.cs は使わないため、削除するか全体をコメントアウトします。テストプロジェクトが RootDialog を参照しているため、ボットアプリだけをビルドして実行します。

エミュレーターでの検証とデバッグ

1. LuisRootDiaog.cs の GetLuisQueryTextAsync メソッドと CreateEventDialog の StartAsync にブレークポイントを置きます。

2. エミュレータで接続して、「金曜日午後7時から、同僚とステーキを食べに行く」と入力して送ります。ブレークしたらテキストを確認。

3. 認証が必要な場合は認証を行って続行。CreateEventDialog に来ても、元の文章が保持されていることを確認。

4. LuisResult では翻訳された文章が渡されていることを確認。

5. 実行してエミュレータに表示される質問を確認。

6. この時点ですでに入っているデータを確認するため、「ステータス」と送信。Subject が取れているため、英語になっていますが、とりあえず進めます。

7. ステーキ食べるのはきっと 3 時間くらいなので、「3」を送信。

8. 作成されたイベントを確認。

テストの作成

ユニットテスト

1. UnitTest1.cs はもう不要なのでコメントアウトしておきます。

2. Mock された LUIS を LUIS ダイアログに渡せるよう、LuisUnitTest1.cs にコンストラクターを追加します。

public LuisRootDialog(params ILuisService[] services) :base(services)
{
}

3. LuisUnitTest1.cs を以下のコードと差し替えます。基本的には UnitTest1.cs でやっていた事と同じですが、LUIS で Entity を返すテストが入ります。簡略化のため、他のテストはここには入れていませんが、自分で書いてみてください。また datetimeV2 に対応していないため、ちょっと書くのが面倒です。。

using Autofac;
using Microsoft.Bot.Builder.Base;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Builder.Internals.Fibers;
using Microsoft.Bot.Builder.Luis;
using Microsoft.Bot.Builder.Luis.Models;
using Microsoft.Bot.Builder.Tests;
using Microsoft.Bot.Connector;
using Microsoft.Graph;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json.Linq;
using O365Bot.Dialogs;
using O365Bot.Handlers;
using O365Bot.Resources;
using O365Bot.Services;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace O365Bot.UnitTests
{
    [TestClass]
    public class SampleLuisTest : LuisTestBase
    {
        private string locale = "ja-JP";

        public SampleLuisTest()
        {            
            Thread.CurrentThread.CurrentCulture = new CultureInfo(locale);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(locale);
        }

        [TestMethod]
        public async Task ShouldReturnEvents()
        {
            // Fakes を使うためにコンテキストを作成
            using (ShimsContext.Create())
            {
                // AuthBot の GetAccessToken メソッドを実行した際、dummyToken というトークンが返るよう設定
                AuthBot.Fakes.ShimContextExtensions.GetAccessTokenIBotContextString = 
                    async (a, e) => { return "dummyToken"; };
                
                // LUIS サービスのモック
                var luis1 = new Mock();
                // 他依存サービスのモック
                var mockEventService = new Mock();
                mockEventService.Setup(x => x.GetEvents()).ReturnsAsync(new List()
                {
                    new Event
                    {
                        Subject = "dummy event",
                        Start = new DateTimeTimeZone()
                        {
                            DateTime = "2017-05-31 12:00",
                            TimeZone = "Standard Tokyo Time"
                        },
                        End = new DateTimeTimeZone()
                        {
                            DateTime = "2017-05-31 13:00",
                            TimeZone = "Standard Tokyo Time"
                        }
                    }
                });
                var subscriptionId = Guid.NewGuid().ToString();
                var mockNotificationService = new Mock();
                mockNotificationService.Setup(x => x.SubscribeEventChange()).ReturnsAsync(subscriptionId);
                mockNotificationService.Setup(x => x.RenewSubscribeEventChange(It.IsAny())).Returns(Task.FromResult(true));

                var builder = new ContainerBuilder();
                builder.RegisterInstance(mockEventService.Object).As();
                builder.RegisterInstance(mockNotificationService.Object).As();
                WebApiApplication.Container = builder.Build();

                // テストしたい LUIS ダイアログのインスタンス作成
                LuisRootDialog rootDialog = new LuisRootDialog(luis1.Object);

                // メモリ内で実行できる環境を作成
                Func> MakeRoot = () => rootDialog;
                using (new FiberTestBase.ResolveMoqAssembly(luis1.Object))
                using (var container = Build(Options.ResolveDialogFromContainer, luis1.Object))
                {
                    var dialogBuilder = new ContainerBuilder();
                    dialogBuilder
                        .RegisterInstance(rootDialog)
                        .As>();
                    dialogBuilder.Update(container);

                     // グローバルメッセージ登録
                    RegisterBotModules(container);

                    // LUIS の結果を GetEvents に指定
                    SetupLuis(luis1, d => d.GetEvents(null, null, null), 1.0, null);
                    
                    // Bot に送るメッセージを作成
                    var toBot = DialogTestBase.MakeTestMessage();
                    toBot.From.Id = Guid.NewGuid().ToString();
                    // Locale 設定
                    toBot.Locale = locale;
                    toBot.Text = "予定一覧";

                    // メッセージを送信して、結果を受信
                    IMessageActivity toUser = await GetResponse(container, MakeRoot, toBot);

                    // 結果の検証
                    Assert.IsTrue(toUser.Text.Equals("2017-05-31 12:00-2017-05-31 13:00: dummy event"));
                }
            }
        }

        [TestMethod]
        public async Task ShouldCreateAllDayEvent()
        {
            // Fakes を使うためにコンテキストを作成
            using (ShimsContext.Create())
            {
                // AuthBot の GetAccessToken メソッドを実行した際、dummyToken というトークンが返るよう設定
                AuthBot.Fakes.ShimContextExtensions.GetAccessTokenIBotContextString =
                    async (a, e) => { return "dummyToken"; };

                // LUIS サービスのモック
                var luis1 = new Mock();
                // 他依存サービスのモック

                // サービスのモック
                var mockEventService = new Mock();
                mockEventService.Setup(x => x.CreateEvent(It.IsAny())).Returns(Task.FromResult(true));
                var subscriptionId = Guid.NewGuid().ToString();
                var mockNotificationService = new Mock();
                mockNotificationService.Setup(x => x.SubscribeEventChange()).ReturnsAsync(subscriptionId);
                mockNotificationService.Setup(x => x.RenewSubscribeEventChange(It.IsAny())).Returns(Task.FromResult(true));
                var mockTranslationService = new Mock();
                mockTranslationService.Setup(x => x.Translate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync("find  sentence");

                var builder = new ContainerBuilder();
                builder.RegisterInstance(mockEventService.Object).As();
                builder.RegisterInstance(mockNotificationService.Object).As();
                builder.RegisterInstance(mockTranslationService.Object).As();
                WebApiApplication.Container = builder.Build();

                // テストしたい LUIS ダイアログのインスタンス作成
                LuisRootDialog rootDialog = new LuisRootDialog(luis1.Object);

                // メモリ内で実行できる環境を作成
                Func> MakeRoot = () => rootDialog;
                using (new FiberTestBase.ResolveMoqAssembly(luis1.Object))
                using (var container = Build(Options.ResolveDialogFromContainer, luis1.Object))
                {
                    var dialogBuilder = new ContainerBuilder();
                    dialogBuilder
                        .RegisterInstance(rootDialog)
                        .As>();
                    dialogBuilder.Update(container);

                    // グローバルメッセージ登録
                    RegisterBotModules(container);

                    // datetimeV2 の resolution 作成
                    Dictionary resolution = new Dictionary();
                    JArray values = new JArray();
                    Dictionary resolutionData = new Dictionary();
                    resolutionData.Add("type", "date");
                    resolutionData.Add("value", DateTime.Now);
                    values.Add(JToken.FromObject(resolutionData));                    
                    resolution.Add("values", values);

                    // LUIS の結果を CreateEvent に指定し、Entity を設定
                    SetupLuis(luis1, d => d.CreateEvent(null, null, null), 1.0,
                        EntityFor("builtin.datetimeV2.date", "", resolution),
                        EntityFor("Calendar.Subject", "dummy subject"));
                    
                    // Bot に送るメッセージを作成
                    var toBot = DialogTestBase.MakeTestMessage();
                    // ロケールで日本語を指定
                    toBot.Locale = locale;
                    toBot.From.Id = Guid.NewGuid().ToString();
                    toBot.Text = "7月1日に食事に行く";

                    // メッセージを送信して、結果を受信
                    var toUser = await GetResponses(container, MakeRoot, toBot);

                    // 結果の検証
                    Assert.IsTrue(toUser.Last().Text.Equals(O365Bot_Models_OutlookEvent.Hours_promptDefinition_LIST));

                    toBot.Text = "3";
                    toUser = await GetResponses(container, MakeRoot, toBot);
                    Assert.IsTrue(toUser.First().Text.Equals(O365BotLabel.Event_Created));
                }
            }
        }

        [TestMethod]
        public async Task ShouldCreateEvent()
        {
            // Fakes を使うためにコンテキストを作成
            using (ShimsContext.Create())
            {
                // AuthBot の GetAccessToken メソッドを実行した際、dummyToken というトークンが返るよう設定
                AuthBot.Fakes.ShimContextExtensions.GetAccessTokenIBotContextString =
                    async (a, e) => { return "dummyToken"; };

                // LUIS サービスのモック
                var luis1 = new Mock();
                // 他依存サービスのモック

                // サービスのモック
                var mockEventService = new Mock();
                mockEventService.Setup(x => x.CreateEvent(It.IsAny())).Returns(Task.FromResult(true));
                var subscriptionId = Guid.NewGuid().ToString();
                var mockNotificationService = new Mock();
                mockNotificationService.Setup(x => x.SubscribeEventChange()).ReturnsAsync(subscriptionId);
                mockNotificationService.Setup(x => x.RenewSubscribeEventChange(It.IsAny())).Returns(Task.FromResult(true));
                var mockTranslationService = new Mock();
                mockTranslationService.Setup(x => x.Translate(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync("find  sentence");

                var builder = new ContainerBuilder();
                builder.RegisterInstance(mockEventService.Object).As();
                builder.RegisterInstance(mockNotificationService.Object).As();
                builder.RegisterInstance(mockTranslationService.Object).As();
                WebApiApplication.Container = builder.Build();

                // テストしたい LUIS ダイアログのインスタンス作成
                LuisRootDialog rootDialog = new LuisRootDialog(luis1.Object);

                // メモリ内で実行できる環境を作成
                Func> MakeRoot = () => rootDialog;
                using (new FiberTestBase.ResolveMoqAssembly(luis1.Object))
                using (var container = Build(Options.ResolveDialogFromContainer, luis1.Object))
                {
                    var dialogBuilder = new ContainerBuilder();
                    dialogBuilder
                        .RegisterInstance(rootDialog)
                        .As>();
                    dialogBuilder.Update(container);

                    // グローバルメッセージ登録
                    RegisterBotModules(container);

                    // datetimeV2 の resolution 作成
                    Dictionary resolution = new Dictionary();
                    JArray values = new JArray();
                    Dictionary resolutionData = new Dictionary();
                    resolutionData.Add("type", "date");
                    resolutionData.Add("value", DateTime.Now.Date);
                    values.Add(JToken.FromObject(resolutionData));
                    resolution.Add("values", values);

                    // LUIS の結果を CreateEvent に指定し、Entity を設定
                    SetupLuis(luis1, d => d.CreateEvent(null, null, null), 1.0,
                        EntityFor("builtin.datetimeV2.date", "", resolution),
                        EntityFor("Calendar.Subject", "dummy subject"));

                    // Bot に送るメッセージを作成
                    var toBot = DialogTestBase.MakeTestMessage();
                    // ロケールで日本語を指定
                    toBot.Locale = locale;
                    toBot.From.Id = Guid.NewGuid().ToString();
                    toBot.Text = "7月1日に食事に行く";

                    // メッセージを送信して、結果を受信
                    var toUser = await GetResponses(container, MakeRoot, toBot);

                    // 結果の検証
                    Assert.IsTrue(toUser.Last().Text.Equals(O365BotLabel.Event_Created));
                }
            }
        }

        /// 
        /// グローバルメッセージおよびインターセプター登録
        /// 
        private void RegisterBotModules(IContainer container)
        {
            var builder = new ContainerBuilder();
            // グローバルメッセージの処理登録
            builder.RegisterModule(new ReflectionSurrogateModule());
            builder.RegisterModule();
            // インターセプトの登録
            builder.RegisterType().AsImplementedInterfaces().InstancePerDependency();
            builder.Update(container);
        }

        /// 
        /// Bot にメッセージを送って、結果を受信
        /// 
        public async Task GetResponse(IContainer container, Func> makeRoot, IMessageActivity toBot)
        {
            using (var scope = DialogModule.BeginLifetimeScope(container, toBot))
            {
                DialogModule_MakeRoot.Register(scope, makeRoot);

                // act: sending the message
                using (new LocalizedScope(toBot.Locale))
                {
                    var task = scope.Resolve();
                    await task.PostAsync(toBot, CancellationToken.None);
                }
                //await Conversation.SendAsync(toBot, makeRoot, CancellationToken.None);
                return scope.Resolve>().Dequeue();
            }
        }

        /// 
        /// Bot にメッセージを送って、結果を受信
        /// 
        public async Task> GetResponses(IContainer container, Func> makeRoot, IMessageActivity toBot)
        {
            using (var scope = DialogModule.BeginLifetimeScope(container, toBot))
            {
                var results = new List();
                DialogModule_MakeRoot.Register(scope, makeRoot);

                // act: sending the message
                using (new LocalizedScope(toBot.Locale))
                {
                    var task = scope.Resolve();
                    await task.PostAsync(toBot, CancellationToken.None);
                }
                //await Conversation.SendAsync(toBot, makeRoot, CancellationToken.None);
                var queue= scope.Resolve>();
                while(queue.Count != 0)
                {
                    results.Add(queue.Dequeue());
                }

                return results;
            }
        }

        /// 
        /// プロアクティブ通知でダイアログ差し込みを行い、結果を受信
        /// 
        public async Task> Resume(IContainer container, IDialog dialog, IMessageActivity toBot)
        {
            using (var scope = DialogModule.BeginLifetimeScope(container, toBot))
            {
                var results = new List();

                var botData = scope.Resolve();
                await botData.LoadAsync(CancellationToken.None);
                var task = scope.Resolve();

                //現在の会話にダイアログを差し込み
                task.Call(dialog.Void(), null);
                await task.PollAsync(CancellationToken.None);
                await botData.FlushAsync(CancellationToken.None);
                
                // 結果の取得
                var queue = scope.Resolve>();
                while (queue.Count != 0)
                {
                    results.Add(queue.Dequeue());
                }

                return results;
            }
        }        
    }
}

ファンクションテスト

ファンクションテストも同様に変更してください。

まとめ

LUIS を使うと一気に高度になった感じがあります。BotBuilder で datatimeV2 に対応してくれると、テストコードもシンプルに書けそうです。

Share the post

Bot Framework と Microsoft Graph で DevOps その 19 : LUIS と多言語対応ボットの実装編

×

Subscribe to Msdn Blogs | Get The Latest Information, Insights, Announcements, And News From Microsoft Experts And Developers In The Msdn Blogs.

Get updates delivered right to your inbox!

Thank you for your subscription

×