Alice em Kotlin: transformando o código em Yandex.Station



Em junho, Yandex organizou uma hackatona online entre desenvolvedores de habilidades de voz. Nós da Just AI estávamos atualizando nossa estrutura de código aberto em Kotlin para oferecer suporte aos novos recursos interessantes de Alice. E foi necessário criar algum tipo de exemplo simples para o README ...



Sobre como algumas centenas de linhas de código em Kotlin se transformaram em Yandex.Station



Alice + Kotlin = JAICF



Just AI possui um framework de código aberto e totalmente gratuito para o desenvolvimento de aplicativos de voz e chatbots de texto - JAICF . Ele foi escrito em Kotlin , uma linguagem de programação da JetBrains, que é bem conhecida por todos os androids e servidores que escrevem uma empresa sangrenta (bem, ou reescrevem em Java). O framework visa facilitar a criação de aplicativos precisamente conversacionais para vários assistentes de voz, texto e até mesmo telefone.



Yandex tem Alice, uma assistente de voz com uma voz agradável e uma API aberta para desenvolvedores terceirizados. Ou seja, qualquer desenvolvedor pode expandir a funcionalidade de Alice para milhões de usuários e até mesmo obter dinheiro do Yandex para isso .



Nós clarofez oficialmente JAICF amigo de Alice , então agora você pode escrever habilidades em Kotlin. E é assim que parece.



Script -> Webhook -> Diálogo





Qualquer habilidade de Alicia é um diálogo de voz entre um usuário e um assistente digital. O diálogo é descrito em JAICF na forma de scripts, que são executados no servidor webhook, que é registrado em Yandex.Dialogues.



Cenário



Vamos pegar uma habilidade que inventamos para um hackathon. Ajuda a economizar dinheiro ao fazer compras nas lojas. Primeiro, veja como funciona.





Aqui você pode ver como o usuário pergunta a Alice - "Diga-me o que é mais lucrativo - tantos rublos por tal e tal quantia ou tanto por isso?"



Alice imediatamente lança nossa habilidade (porque é chamada de "O que é mais lucrativo") e transfere todas as informações necessárias para ela - a intenção do usuário e os dados de sua solicitação .



A habilidade, por sua vez, reage à intenção, processa os dados e retorna uma resposta útil. Alice diz a resposta e desliga porque a habilidade encerra a sessão (eles chamam isso de “habilidade de uma passagem”).



Aqui está um cenário tão simples, que, no entanto, permite calcular rapidamente quanto um produto é mais lucrativo do que outro. E, ao mesmo tempo, ganhe uma coluna de palestras da Yandex.




Como é em 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.")
        }
    }
}




O script completo está disponível no Github .



Como você pode ver, este é um objeto regular que estende a classe Scenario da biblioteca JAICF. Basicamente, o script é uma máquina de estado, onde cada nó é um estado possível da conversação. É assim que implementamos o trabalho com o contexto, uma vez que o contexto do diálogo é um componente muito importante de qualquer aplicação de voz.



Digamos que a mesma frase possa ser interpretada de maneira diferente dependendo do contexto do diálogo. A propósito, esta é uma das razões pelas quais escolhemos Kotlin para nosso framework - ele permite que você crie uma DSL lacônica , na qual é conveniente gerenciar tais contextos aninhados e transições entre eles.



O estado é ativado comativador (por exemplo, um intent ) e executa o bloco de código aninhado - a ação . E dentro da ação, você pode fazer o que quiser, mas o principal é devolver alguma resposta útil ao usuário ou interrogar algo. Isso é feito por meio de reações . Siga os links para encontrar uma descrição detalhada de cada uma dessas entidades.



Intenções e slots







Uma intenção é uma representação independente do idioma de uma solicitação do usuário. Na verdade, esse é o identificador do que o usuário deseja obter de seu aplicativo de conversação.



Alice recentemente aprendeu como definir intenções automaticamente para sua habilidade se você primeiro descrever uma gramática especial. Além disso, ela sabe extrair os dados necessários da frase em forma de slots - por exemplo, o preço e o volume da mercadoria, como no nosso exemplo.



Para fazer tudo funcionar, você precisa descrever essa gramática e os slots . Esta é a gramática em nossa habilidade, e estes são os slotsnós o usamos nele. Isso permite que nossa habilidade receba na entrada não apenas uma linha de um pedido do usuário em russo, mas um identificador já independente do idioma e slots convertidos adicionalmente (o preço de cada produto e seu volume).



JAICF, é claro, oferece suporte a qualquer outro mecanismo NLU (por exemplo, Caila ou Dialogflow ), mas em nosso exemplo, queríamos usar esse recurso específico do Alice para mostrar como ele funciona.



Webhook



Ok, temos o roteiro. Como verificamos se funciona?



Claro, os adeptos da abordagem de desenvolvimento orientado a testes apreciarão a presença de um mecanismo de teste automatizado integrado para scripts interativos em JAICF , que usamos pessoalmente constantemente, uma vez que fazemos grandes projetos e é difícil verificar todas as alterações manualmente. Mas nosso exemplo é bem pequeno, então é melhor iniciar o servidor imediatamente e tentar falar com Alice.



Para executar o script, você precisa de um webhook - um servidor que aceita solicitações de entrada do Yandex quando o usuário começa a falar com sua habilidade. O servidor não é nada difícil de iniciar - você só precisa configurar seu bot e travar algum endpoint nele.



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


É assim que o bot é configurado - aqui descrevemos quais scripts são usados ​​nele, onde armazenar dados do usuário e quais ativadores precisamos para o script funcionar (pode haver vários deles).



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


E é assim que um servidor com um webhook é inicializado exatamente assim - você só precisa especificar em qual canal e qual ponto de extremidade deve funcionar. Executamos o servidor JetBrains Ktor aqui, mas você pode usar qualquer outro no JAICF .



Aqui, usamos mais um recurso de Alice - armazenar dados do usuário em seu banco de dados interno (opção useDataStorage ). JAICF salvará e restaurará automaticamente o contexto de lá e tudo que nosso script escreve lá. A serialização é transparente.



Diálogo



Finalmente podemos testar tudo! O servidor é executado localmente, portanto, precisamos de um URL público temporário para que as solicitações de Alice acessem nosso webhook da Internet. Para fazer isso, é conveniente usar a ferramenta gratuita ngrok , simplesmente executando um comando no terminal como ngrok http 8080







Todos os pedidos chegarão em tempo real no seu PC - para que você possa depurar e editar o código.



Agora você pode pegar o URL https recebido e especificá-lo ao criar um novo diálogo Aliego no Yandex. Diálogos . Lá você também pode testar o diálogo com texto. Mas se você quiser falar com uma habilidade com voz, agora Alice pode publicar habilidades privadas rapidamente, que no momento do desenvolvimento estão disponíveis apenas para você. Então, sem passar por uma longa moderação do Yandex, você já pode começar a falar com sua habilidade diretamente do aplicativo de Alice ou de um alto-falante inteligente.







Publicação



Testamos tudo e estamos prontos para publicar a habilidade para todos os usuários do Alice! Para fazer isso, nosso webhook deve ser hospedado em algum lugar em um servidor público com uma URL constante. Em princípio, os aplicativos em JAICF podem ser executados em qualquer lugar onde o Java seja compatível (mesmo em um smartphone Android).



Corremos nosso exemplo no Heroku . Acabamos de criar um novo aplicativo e registrar o endereço de nosso repositório Github onde o código de habilidade está armazenado. O Heroku constrói e executa tudo a partir da própria fonte. Só temos que registrar o URL público resultante no Yandex. Diálogos e enviam tudo para moderação .



Total



Este pequeno tutorial segue os passos do hackathon Yandex , onde o cenário acima “ Qual é mais lucrativo ” ganhou uma das três Yandex.Stations! Aqui, aliás, você pode ver como foi .



O framework JAICF no Kotlin me ajudou a implementar e depurar rapidamente o script de diálogo, sem me preocupar em trabalhar com a API, contextos e bancos de dados de Alice, ao mesmo tempo que não limita as possibilidades (como costuma ser o caso com bibliotecas semelhantes).



Links Úteis



O documento JAICF completo está aqui .

As instruções para criar habilidades nele para Alice estão aqui .

A fonte da habilidade em si pode ser encontrada .



E se você gostou



Sinta-se à vontade para contribuir com o JAICF , como os colegas do Yandex já estão fazendo , ou apenas deixe um asterisco no Github .



E se você tiver alguma dúvida, nós respondemos imediatamente em nosso aconchegante Slack .



All Articles