KotlinのAlice:コードをYandex.Stationに変換する



6月、Yandex音声スキル開発者の間でオンラインハッカソンを主催しましたJust AIは、Kotlinのオープンソースフレームワークを更新して、Aliceのクールな新機能をサポートしていました。そして、READMEの簡単な例を考え出す必要がありました... Kotlinの数百行のコードがYandex.Stationに



どのように変わったかについて



アリス+コトリン= JAICF



Just AIには、音声アプリケーションとテキストチャットボットを開発するためのオープンソースで完全に無料のフレームワークがあります-JAICF。これは、JetBrainsのプログラミング言語であるKotlin書かれています。これは、流血の企業を作成するすべてのAndroidとサーバーによく知られています(またはJavaから書き直します)。このフレームワークは、さまざまな音声、テキスト、さらには電話アシスタント向けの正確な会話型アプリケーションの作成を容易にすることを目的としています。



Yandexには、心地よい声とサードパーティ開発者向けのオープンAPI備えた音声アシスタントであるAliceがいます。つまり、どの開発者も何百万ものユーザーのためにAliceの機能を拡張でき、Yandexからこのためのお金を得ることができます



もちろん正式にアリスJAICFの友達ができたので、コトリンでスキルを書くことができます。そして、これはそれがどのように見えるかです。



スクリプト-> Webhook->ダイアログ





Aliciaのスキルは、ユーザーとデジタルアシスタントの間の音声対話です。ダイアログはJAICFでスクリプトの形式で記述され、スクリプトはYandex.Dialoguesに登録されているWebhookサーバーで実行されます。



シナリオ



ハッカソンのために思いついたスキルを取りましょう。店舗で買い物をするときにお金を節約するのに役立ちます。まず、それがどのように機能するかを見てください。





ここでは、ユーザーがアリスにどのように質問するかを見ることができます- 「もっと有益なものを教えてください-そのような量のルーブルがたくさんありますか?」



アリスはすぐに私たちのスキルを起動し(「より収益性の高いもの」と呼ばれるため)、必要なすべての情報(ユーザーの意図と要求からのデータ)をそれに転送します。



次に、スキルは意図に反応し、データを処理して、有用な応答を返します。スキルがセッションを終了するため、アリスは答えを言ってオフにします(彼らはこれを「ワンパススキル」と呼びます)。



これはそのような単純なシナリオですが、ある製品が別の製品よりどれだけ収益性が高いかをすばやく計算することができます。同時に、Yandexからスピーキングコラムを獲得します。




コトリンではどのように見えますか?
object MainScenario: Scenario() {
    init {
        state("profit") {
            activators {
                intent("CALCULATE.PROFIT")
            }

            action {
                activator.alice?.run {
                    val a1 = slots["first_amount"]
                    val a2 = slots["second_amount"]
                    val p1 = slots["first_price"]
                    val p2 = slots["second_price"]
                    val u1 = slots["first_unit"]
                    val u2 = slots["second_unit"] ?: firstUnit

                    context.session["first"] = Product(a1?.value?.double ?: 1.0, p1!!.value.int, u1!!.value.content)
                    context.session["second"] = p2?.let {
                        Product(a2?.value?.double ?: 1.0, p2.value.int, u2!!.value.content)
                    }

                    reactions.go("calculate")
                }
            }

            state("calculate") {
                action {
                    val first = context.session["first"] as? Product
                    val second = context.session["second"] as? Product

                    if (second == null) {
                        reactions.say("   ?")
                    } else {
                        val profit = try {
                            ProfitCalculator.calculateProfit(first!!, second)
                        } catch (e: Exception) {
                            reactions.say("   , .   .")
                            return@action
                        }

                        if (profit == null || profit.percent == 0) {
                            reactions.say("     .")
                        } else {
                            val variant = when {
                                profit.product === first -> ""
                                else -> ""
                            }

                            var reply = "$variant   "

                            reply += when {
                                profit.percent < 10 -> "   ${profit.percent}%."
                                profit.percent < 100 -> " ${profit.percent}%."
                                else -> "  ${profit.percent}%."
                            }

                            context.client["last_reply"] = reply
                            reactions.say(reply)
                            reactions.alice?.endSession()
                        }
                    }
                }
            }

            state("second") {
                activators {
                    intent("SECOND.PRODUCT")
                }

                action {
                    activator.alice?.run {
                        val a2 = slots["second_amount"]
                        val p2 = slots["second_price"]
                        val u2 = slots["second_unit"]

                        val first = context.session["first"] as Product
                        context.session["second"] = Product(
                            a2?.value?.double ?: 1.0,
                            p2!!.value.int,
                            u2?.value?.content ?: first.unit
                        )

                        reactions.go("../calculate")
                    }
                }
            }
        }

        fallback {
            reactions.say(",   . " +
                    "  :  , 2   230   3   400.")
        }
    }
}




完全なスクリプトはGithubで入手できます



ご覧のとおり、これはJAICFライブラリのScenarioクラスを拡張する通常のオブジェクトです。基本的に、スクリプトは状態マシンであり、各ノードは会話の可能な状態です。対話のコンテキストは音声アプリケーションの非常に重要なコンポーネントであるため、これがコンテキストを使用して作業を実装する方法です。



同じフレーズが、対話のコンテキストに応じて異なって解釈される可能性があるとしましょう。ちなみに、これがフレームワークにKotlinを選択した理由の1つです。これにより、このようなネストされたコンテキストとそれらの間の遷移を管理するのに便利なlaconicDSLを作成できます



状態はでアクティブ化されますアクティベーター(たとえば、インテント)を実行し、ネストされたコードブロック(アクション)を実行しますアクション内では、好きなことを行うことができますが、主なことは、ユーザーに役立つ回答を返すか、何かを調べることです。これは反応を通じて行われます。これらの各エンティティの詳細な説明については、リンクをたどってください。



インテントとスロット







インテントは、ユーザーリクエストの言語に依存しない表現です。実際には、これはユーザーが会話型アプリケーションから取得したいものの識別子です。



アリスは最近、特別な文法を最初に説明する場合に、スキルの意図を自動的に定義する方法を学びました。さらに、彼女は、スロットの形式でフレーズから必要なデータを抽出する方法を知っています。たとえば、この例のように、商品の価格と数量です。



すべてを機能させるには、そのような文法スロットを記述する必要がありますこれは私たちのスキルの文法であり、これらはスロットですその中で使用しています。これにより、入り口でロシア語のユーザーリクエストの行だけでなく、すでに言語に依存しない識別子と変換されたスロット(各製品の価格とそのボリューム)を受け取ることができます。



もちろん、JAICFは他のNLUエンジンCailaDialogflowなど)をサポートしますが、この例では、この特定のAlice機能を使用して彼女の動作を示したいと思いました。



Webhook



さて、スクリプトがあります。それが機能することをどのように確認しますか?



もちろん、テスト駆動型開発アプローチの支持者は、大規模なプロジェクトを行っており、すべての変更を手作業で確認することは困難であるため、私たちが常に使用しているJAICFのインタラクティブスクリプト用の組み込み自動テストメカニズムの存在を高く評価します。しかし、私たちの例は非常に小さいので、すぐにサーバーを起動して、アリスと話をしてみたほうがいいでしょう。



スクリプトを実行するには、Webhook(ユーザーがスキルと話し始めたときにYandexからの着信要求を受け入れるサーバー)が必要です。サーバーの起動はまったく難しくありません。ボットを構成し、エンドポイントをハングアップするだけです。



val skill = BotEngine(
    model = MainScenario.model,
    activators = arrayOf(
        AliceIntentActivator,
        BaseEventActivator,
        CatchAllActivator
    )
)


これがボットの構成方法です。ここでは、ボットで使用されるスクリプト、ユーザーデータを格納する場所、およびスクリプトが機能するために必要なアクティベーターについて説明します(いくつかある場合があります)。



fun main() {
    embeddedServer(Netty, System.getenv("PORT")?.toInt() ?: 8080) {
        routing {
            httpBotRouting("/" to AliceChannel(skill, useDataStorage = true))
        }
    }.start(wait = true)
}


しかし、これはWebhookを備えたサーバーがそのように起動する方法です-どのエンドポイントでどのチャネルが機能するかを指定する必要がありますここではJetBrainsKtorサーバーを実行しましたが、JAICFでは他のサーバーを使用できます



ここでは、アリスのもう1つの機能である内部データベースへのユーザーデータの保存を使用しましたuseDataStorageオプション)。JAICFは、そこからコンテキストとスクリプトがそこに書き込むすべてのものを自動的に保存および復元しますシリアル化は透過的です。



ダイアログ



最後に、すべてをテストできます。サーバーはローカルで実行されるため、インターネットからWebhookに到達するためのAliceの要求には一時的なパブリックURLが必要です。これを行うには、無料のngrokツールを使用すると便利です。端末でコマンドを実行するだけで、ngrok http 8080







すべてのリクエストがPCにリアルタイムで到着するため、コードをデバッグおよび編集できます。



これで、受信したhttps URLを取得して、Yandexで新しいAliegoダイアログを作成するときに指定できます。対話。そこで、テキストを使用してダイアログをテストすることもできます。しかし、声でスキルと話したい場合は、アリスがプライベートスキルをすばやく公開できるようになりました、開発時にはあなただけが利用できます。したがって、Yandexからの長いモデレートを経ることなく、アリスのアプリケーションまたはスマートスピーカーから直接スキルと話し始めることができます。







出版物



私たちはすべてをテストし、すべてのアリスユーザーにスキルを公開する準備ができました!これを行うには、Webhookを一定のURLを持つパブリックサーバーのどこかでホストする必要があります。原則として、JAICF上のアプリケーションは、Javaがサポートされている場所であればどこでも実行できます(Androidスマートフォンでも)。Herokuで



例を実行しました新しいアプリケーションを作成し、スキルコードが保存されているGithubリポジトリのアドレスを登録しましたHerokuは、ソース自体からすべてを構築して実行します。結果のパブリックURLをYandexに登録する必要があります。対話し、モデレートのためにすべてを送信します



合計



この小さなチュートリアルは、Yandexハッカソンの足跡をたどります。ここでは、上記のシナリオ「どちらがより収益性が高いか」が3つのYandex.Stationの1つを獲得しました。ちなみに、ここではどうだっかがわかります



KotlinのJAICFフレームワークは、可能性を制限することなく(同様のライブラリでよくあることですが)、AliceのAPI、コンテキスト、およびデータベースの操作に煩わされることなく、ダイアログスクリプトをすばやく実装およびデバッグするのに役立ちました。



便利なリンク



完全なJAICFドキュメントはこちらです。

アリスのためにスキルを作成するための手順はこちらです。

スキル自体のソースはそこにあります。



そして、あなたが好きなら



Yandexの同僚すでに行っているようにJAICFに 自由に貢献するかGithubにアスタリスクを残してください ご不明な点がございましたら、居心地の良いSlackですぐお答えします






All Articles