Alice on Kotlin: transformer le code en Yandex.Station



En juin, Yandex a organisé un hackathon en ligne parmi les développeurs de compétences vocales. Chez Just AI, nous étions en train de mettre à jour notre framework open source sur Kotlin pour prendre en charge de nouvelles fonctionnalités intéressantes d'Alice. Et il était nécessaire de trouver une sorte d'exemple simple pour le README ...



sur la façon dont quelques centaines de lignes de code sur Kotlin se sont transformées en Yandex.Station



Alice + Kotlin = JAICF



Just AI a un framework open source et entièrement gratuit pour développer des applications vocales et des chatbots textuels - JAICF . Il est écrit en Kotlin , un langage de programmation de JetBrains, bien connu de tous les androïdes et serveurs qui écrivent une entreprise sanglante (enfin, ou la réécrivent à partir de Java). Le cadre vise à faciliter la création d'applications conversationnelles précises pour divers assistants vocaux, textuels et même téléphoniques.



Yandex a Alice, un assistant vocal avec une voix agréable et une API ouverte pour les développeurs tiers. Autrement dit, tout développeur peut étendre les fonctionnalités d'Alice pour des millions d'utilisateurs et même obtenir de l'argent de Yandex pour cela .



Nous bien sûrs'est officiellement lié d'amitié JAICF avec Alice , donc maintenant vous pouvez écrire des compétences en Kotlin. Et voici à quoi ça ressemble.



Script -> Webhook -> Dialogue





Toute compétence Alicia est un dialogue vocal entre un utilisateur et un assistant numérique. La boîte de dialogue est décrite dans JAICF sous forme de scripts, qui sont ensuite exécutés sur le serveur webhook, qui est enregistré dans Yandex.Dialogues.



Scénario



Prenons une compétence que nous avons inventée pour un hackathon. Cela permet d'économiser de l'argent lors de vos achats en magasin. Tout d'abord, voyez comment cela fonctionne.





Ici, vous pouvez voir comment l'utilisateur demande à Alice - "Dites-moi ce qui est le plus rentable - tant de roubles pour tel ou tel montant ou tant pour cela?"



Alice lance immédiatement notre compétence (car elle s'appelle «Qu'est-ce qui est le plus rentable») et lui transfère toutes les informations nécessaires - l' intention de l' utilisateur et les données de sa demande .



La compétence, à son tour, réagit à l'intention, traite les données et renvoie une réponse utile. Alice dit la réponse et s'éteint, car la compétence met fin à la session (ils appellent cette «compétence en un seul passage»).



Voici un scénario aussi simple, qui vous permet cependant de calculer rapidement à quel point un produit est plus rentable qu'un autre. Et en même temps, gagnez une chronique parlante de Yandex.




À quoi ça ressemble à Kotlin?
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.")
        }
    }
}




Le script complet est disponible sur Github .



Comme vous pouvez le voir, il s'agit d'un objet régulier qui étend la classe Scenario de la bibliothèque JAICF. Fondamentalement, le script est une machine à états, où chaque nœud est un état possible de la conversation. C'est ainsi que nous implémentons le travail avec le contexte, puisque le contexte de dialogue est un composant très important de toute application vocale.



Disons qu'une même phrase peut être interprétée différemment selon le contexte du dialogue. Au fait, c'est l'une des raisons pour lesquelles nous avons choisi Kotlin pour notre framework - cela vous permet de créer un DSL laconique , dans lequel il est pratique de gérer ces contextes imbriqués et les transitions entre eux.



L'état est activé avecactivateur (par exemple, un intent ) et exécute le bloc de code imbriqué - l' action . Et à l'intérieur de l'action, vous pouvez faire ce que vous voulez, mais l'essentiel est de renvoyer une réponse utile à l'utilisateur ou d'interroger quelque chose. Cela se fait par des réactions . Suivez les liens pour une description détaillée de chacune de ces entités.



Intentions et créneaux horaires







Un intent est une représentation indépendante de la langue d'une demande utilisateur. En fait, il s'agit d'un identifiant de ce que l'utilisateur souhaite obtenir de votre application conversationnelle.



Alice a récemment appris à définir automatiquement les intentions de votre compétence si vous décrivez d'abord une grammaire spéciale. De plus, elle sait extraire les données nécessaires de la phrase sous forme de créneaux horaires - par exemple, le prix et le volume des marchandises, comme dans notre exemple.



Pour que tout fonctionne, vous devez décrire une telle grammaire et des emplacements . C'est la grammaire de notre compétence, et ce sont les emplacementsnous l'utilisons. Cela permet à notre compétence de recevoir à l'entrée non seulement une ligne de demande utilisateur en russe, mais un identifiant déjà indépendant de la langue et des slots convertis en plus (le prix de chaque produit et son volume).



JAICF, bien sûr, prend en charge tout autre moteur NLU (par exemple, Caila ou Dialogflow ), mais dans notre exemple, nous voulions utiliser cette fonctionnalité particulière d'Alice pour montrer comment cela fonctionne.



Webhook



D'accord, nous avons le script. Comment vérifions-nous que cela fonctionne?



Bien sûr, les adeptes de l' approche de développement piloté par les tests apprécieront la présence d'un mécanisme de test automatisé intégré pour les scripts interactifs dans JAICF , que nous utilisons personnellement en permanence, car nous réalisons de grands projets, et il est difficile de vérifier tous les changements à la main. Mais notre exemple est assez petit, donc nous ferions mieux de démarrer le serveur tout de suite et d'essayer de parler à Alice.



Pour exécuter le script, vous avez besoin d'un webhook - un serveur qui accepte les demandes entrantes de Yandex lorsque l'utilisateur commence à parler avec votre compétence. Le serveur n'est pas du tout difficile à démarrer - il vous suffit de configurer votre bot et d'y accrocher un point de terminaison.



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


Voici comment le bot est configuré - nous décrivons ici quels scripts y sont utilisés, où stocker les données utilisateur et de quels activateurs nous avons besoin pour que le script fonctionne (il peut y en avoir plusieurs).



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


Mais c'est ainsi qu'un serveur avec un webhook démarre comme ça - il vous suffit de spécifier quel canal sur quel point de terminaison doit fonctionner. Nous avons exécuté le serveur JetBrains Ktor ici, mais vous pouvez en utiliser un autre dans JAICF .



Ici, nous avons utilisé une autre fonctionnalité d'Alice - stocker les données utilisateur dans sa base de données interne (option useDataStorage ). JAICF enregistrera et restaurera automatiquement le contexte à partir de là et tout ce que notre script y écrit. La sérialisation est transparente.



Dialogue



On peut enfin tout tester! Le serveur fonctionne localement, nous avons donc besoin d'une URL publique temporaire pour que les requêtes d'Alice atteignent notre webhook depuis Internet. Pour ce faire, il est pratique d'utiliser l' outil gratuit ngrok , simplement en exécutant une commande dans le terminal comme ngrok http 8080







Toutes les demandes arriveront en temps réel sur votre PC - vous pouvez donc déboguer et éditer le code.



Vous pouvez maintenant prendre l'URL https reçue et la spécifier lors de la création d'un nouveau dialogue Aliego sur Yandex. Dialogues . Là, vous pouvez également tester la boîte de dialogue avec du texte. Mais si vous voulez parler à une compétence avec une voix, Alice peut maintenant publier rapidement des compétences privées, qui au moment du développement ne sont disponibles que pour vous. Ainsi, sans passer par une longue modération de Yandex, vous pouvez déjà commencer à parler avec votre compétence directement depuis l'application d'Alice ou depuis un haut-parleur intelligent.







Publication



Nous avons tout testé et sommes prêts à publier la compétence pour tous les utilisateurs d'Alice! Pour ce faire, notre webhook doit être hébergé quelque part sur un serveur public avec une URL constante. En principe, les applications sur JAICF peuvent être exécutées partout où Java est pris en charge (même sur un smartphone Android).



Nous avons exécuté notre exemple sur Heroku . Nous venons de créer une nouvelle application et d'enregistrer l' adresse de notre référentiel Github où le code de compétence est stocké. Heroku construit et exécute tout à partir de la source elle-même. Nous avons juste besoin d'enregistrer l'URL publique résultante dans le Yandex. Dialogues et envoyez le tout pour modération .



Total



Ce petit tutoriel suit les traces du hackathon Yandex , où le scénario ci-dessus « Qui est le plus rentable » a remporté l'une des trois Yandex.Stations! Ici, au fait, vous pouvez voir comment c'était .



Le framework JAICF sur Kotlin m'a aidé à implémenter et déboguer rapidement le script de dialogue, sans me soucier de travailler avec l'API, les contextes et les bases de données d'Alice, sans limiter les possibilités (comme c'est souvent le cas avec des bibliothèques similaires).



Liens utiles



La documentation complète du JAICF est ici .

Les instructions pour créer des compétences pour Alice sont ici .

La source de la compétence elle-même peut être trouvée .



Et si tu as aimé



N'hésitez pas à contribuer à JAICF , comme le font déjà des collègues de Yandex , ou laissez simplement un astérisque sur Github .



Et si vous avez des questions, nous y répondons immédiatement dans notre confortable Slack .



All Articles