JAICF brings Kotlin to Rasa

Today, we're highlighting an unconventional use case developed by the Rasa open source community. In this post, we welcome a guest contributor to the blog, Dmitriy Chechyotkin. Dmitriy discusses making conversational AI accessible to Kotlin developers through JAICF, an open source framework.


Rasa is written in Python, a language that supports a huge community of developers who are keen on NLU algorithms. But who said Python is the only language that can be used to develop conversational solutions using Rasa as the NLU engine?

Rasa’s modular architecture means it can be launched in NLU-only mode, allowing any middleware to use it as a backend service in chatbot or voice assistant projects. This opens the door for actions-level frameworks written in other programming languages, and as a result, expands the Rasa community beyond just Python developers!

Today I’d like to introduce the open-source Just AI Conversational Framework (aka JAICF) that brings the Kotlin language to Rasa, enabling any Android or server-side developer to join the world of conversational and voice-first applications.

Why Kotlin?

According to the Wiki - Kotlin is a cross-platform, statically typed, general-purpose programming language with type inference. It’s developed by JetBrains, which you might be familiar with if you use their brilliant PyCharm IDE. Kotlin is well-known and popular in Android and server-side developer communities. Moreover, Google accepted it as a primary language on I/O 2019.

Context-oriented programming

What makes Kotlin so attractive for conversational application development? The main idea is dialogue context-awareness, meaning that every query a user makes to the conversational app should be processed in the context of the previous dialogue’s state. Kotlin provides a great feature called the context-oriented programming paradigm that enables developers to easily create Domain Specific Languages (DSLs) on top of Kotlin. Here is a very simple example: DSL for HTML pages building.

JAICF as a conversational framework utilizes this (and some other Kotlin features you’ll see below) to provide an interesting tool for context-aware voice and chat agent development.

Simple example

Let me show you a simple example of such a dialogue scenario, written with JAICF DSL.

object MoodScenario: Scenario() {
    init {
        state("mood") {
            action {
                reactions.sayRandom("How are you?", "How are you doing?")
            }

            state("good") {
                activators {
                    intent("mood_great")
                }

                action {
                    reactions.say("Great! Have a nice day!")
                }
            }

            state("bad") {
                activators {
                    intent("mood_unhappy")
                }

                action {
                    reactions.run {
                        sayRandom("Oh no!", "Sad..")
                        image(RandomApi.catImage())
                    }
                }
            }
        }
    }
}

Here you can see how states of the dialogue are organised in terms of parent and inner states. Once the user sends a request that matches the mood_great intent, a corresponding state good is activated and its action block is executed. The mood_unhappy intent activates another state that can also contain its inner states and so on.

What is JAICF?

In Rasa terms, JAICF implements an action layer, meaning that JAICF scenarios contain business logic your bot runs in response to user input. Moreover it implements a dialogue manager meaning that JAICF transparently operates with the current state of the dialogue for every new user request. But what else?

JAICF is a multi-platform framework that enables you to connect your Rasa project to many channels like Google Assistant, Slack or Facebook Messenger. In other words, a single JAICF project can be connected simultaneously to multiple platforms and respond to users’ requests from each of them. The multi-platforming idea isn’t new, except for one thing - JAICF provides an API that doesn’t restrict any platform-specific features, using the power of Kotlin extensions and null-safe operators as you will see below.

How to start using JAICF

Once we’ve briefly learned how JAICF works and what features it contains, we can try it in action.

Template project

To make it easier, we prepared a JAICF+Rasa template project that can be used as a starting point. It describes the process of fast-deploying and using JAICF from scratch. Once you’ve completed the installation, you can test the result via any Telegram client.

By the way, it utilises a ready-to-use Rasa NLU server template that can be deployed to Heroku cloud for any of your projects as well.

Dive into JAICF

Here, we’ll go through the source code of this project and some JAICF concepts to learn how it can be used to build conversational agents in Kotlin.

Development environment

First, you need to clone the source code to your PC as described here, and create a new project from source in IntelliJ IDEA IDE.

IntelliJ IDEA is free on the Community plan and provides the best experience for Kotlin developers.

In order to make changes to the Rasa NLU model, you also need to clone your instance of the Rasa template and run it locally with rasa run --enable-api or via docker as described here.

Scenarios

Each JAICF project contains one or more scenarios that describe the dialogue states and implement some business logic your bot should execute on each user request.

Note that there is a main scenario object that declares top-level dialogue states. These states handle general intents like “greet”, “goodbye” and “bot_challenge” via the activators mechanism. These intents are described in the model.md file of the Rasa project. Please refer to the Rasa tutorial to learn more about how to work with intents and named entities.

There is also a special fallback state which will be invoked each time the user sends a request that cannot be recognised by the current NLU model. Fallbacks in JAICF can be also context-related, meaning that each state can contain its own logic for unrecognised requests.

fallback {
    reactions.run {
        sayRandom("Sorry, I didn't get that...", "Looks like it's something new for me...")
        say("Could you repeat please?")
    }
}

Here you can observe how your bot reacts to the user’s request using the reactions interface. It generates and sends a response to the user, and also provides access to platform-specific features.

Sub-scenarios

Let’s take a look at the MainScenario declaration:

object MainScenario: Scenario(
    dependencies = listOf(MoodScenario)
) {
    ...
}

Here you can see that it depends on another scenario named MoodScenario. This sub-scenario contains reactions for the mood-related branch of the dialogue described by the “mood_great”, “mood_unhappy”, “affirm” and “deny” intents of the NLU model. In this way, JAICF provides a way to separate one single big scenario into the sequence of smaller parts and invoke the sub-scenario by jumping to its state:

action {
    reactions.run {
        sayRandom("Hi there!", "Hello!", "Good day!")
        telegram?.go("/mood")
    }
}

What happens here? The mood scenario declares a top-level state named “mood” and can be activated via the reactions.go(“/mood”) expression. But you can see that the code above uses a different one - reactions.telegram?.go(“/mood”). This is the power of Kotlin in practice—our bot jumps to the mood state only if the user’s request is received through the Telegram platform!

Platform-specific features

Let’s take one more look at the MoodScenario object. Notice how it reacts on the request:

action {
    reactions.telegram?.say(
        text = random("How are you?", "How are you doing?"),
        inlineButtons = listOf("Good", "Bad")
    )
}

Here you can see how the say function differs from the standard one in the reactions interface. The reactions.telegram? expression opens the way to platform-specific methods that can be used to send responses with a particular platform’s features, Telegram’s inline buttons.

NLU features

This template project doesn’t contain any NLU features, like the usage of named entities. But you can learn more about another interface named activator that provides access to the Rasa NLU features via the same activator.rasa? expression.

Running it all together

Now that you’re familiar with the key JAICF concepts, you can make changes to the project’s source code and run it locally or deploy to the cloud. To make it work, you only need to configure the Rasa connector and corresponding channels.

In the TemplateBot.kt file you can see how the RasaApi class can be instantiated by providing an endpoint URL. Note that RasaIntentActivator also should be added to the list of your agent’s activators to make it possible to send and receive responses from the Rasa NLU server.

val rasaApi = RasaApi(System.getenv("RASA_URL") ?: "http://localhost:5005")
val templateBot = BotEngine(
    model = MainScenario.model,
    contextManager = contextManager,
    activators = arrayOf(
        RasaIntentActivator.Factory(rasaApi),
        RegexActivator,
        CatchAllActivator
    )
)

And then run it via your desired channel, like Telegram:

fun main() {
    TelegramChannel(templateBot, System.getenv("TELEGRAM_TOKEN")).run()
}

Conclusion

The Just AI Conversational Framework enables any Kotlin developer to create a conversational project using Rasa NLU and any chat or voice platform. As a middleware between this platform and Rasa, JAICF handles dialogue scenarios written in a declarative manner on a special Kotlin-based DSL. As a multi-platform framework, it provides general purpose response builders as well as platform-specific APIs for each platform, so developers can build rich responses for every platform.

Additional resources