Simple Chatbots with Angular and Ink

von Philipp Siekmann
01.12.2017

After receiving the request from our customer to implement a chat bot for the first time, ideas and possible scenarios unfolded rapidly. Considering the fact that a chat bot enhances the customer’s experience on an e-commerce website, we jotted down ideas; starting with simple dialogs through to implementing rather elaborate conversations that include user input validation and language processing.

To get a good grasp of the challenges and opportunities of creating a chat bot for browsers, we decided to tackle this task from a technical point of view. Hence, one approach would be to dive in, head first, and review the numerous chat bot services and frameworks developed in recent years; a rather expensive and time consuming approach, which would not only require searching through pages of documentation but also installing heavy software infrastructures. Therefore we came up with a more attainable solution i.e. implementing our own small prototypes, which were additionally used in various usability tests to reason about the benefits and pitfalls of using chat bots for various e-commerce-related tasks.

To keep the effort low, we further decided to limit our implementation to solely scripted conversations with simple decision making and input validation, dodging the hard AI parts of conversational UI development such as natural language processing.

TL;DR

We used InkJS in combination with Angular to create a chat bot application featuring simple user input and validation that is easily scriptable by non-programmers.

Here is the code: https://github.com/Sqrrl/angular-ink-chatbot
And here is a demo: https://sqrrl.github.io/angular-ink-chatbot/

Our requirements summed up

First, let's summarize the key features of our prototyping architecture. Our chat bot prototypes should be able to …

  • … present the user with a chat-like conversational UI.
  • … lead the user through scripted conversations.
  • … react to user input in form of predefined options and limited free text.
  • … run validation on user input and branch the conversation accordingly.
  • … allow non-programmers to create and adjust conversation scripts.

1970s text adventures?

While conversational UIs are a relatively new trend in modern interface design, they are definitely not new technology. First conversational interfaces date back to the 70s, before graphical user interfaces became mainstream. Text adventures and interactive fiction provided the user with text-based narratives, prompting them to navigate through complex maps with simple one- or two-word commands.

The thing is, text adventures are still around and resemble our chat bot characteristics quite a lot. That's how we found Ink.

Ink is a scripting and markup language mainly designed for the development of text adventures. With its Inky editor it offers an out of the box solution with a pretty steep learning curve (like in "quick to learn", not "hard to climb"). At its core you write a flow of paragraphs and choices, that allow you to mark up branching dialogue trees while keeping it still readable. You can find the full syntax documentation on Github.

In the second half of this article I will describe how we used and extended the Ink language to build our chat bot prototypes.

Start with Inky

We started evaluating Ink by trying to write a simple chat bot that should demo all of our required features, if possible. First, we used just Inky and its preview function to see how far we would come. Trying to keep it simple, we came up with a bot that first asks the user about his current mood and his birthday, then offers him a cocktail recipe. Using the birthday to calculate the user's age, the bot should be able to present an age-appropriate recipe.

A sample conversation could look something like that:

<code>Bot: Hello! How are you? User: I'm fine. Bot: What's your birthday? User: 05/16/1976 Bot: So you are 41 years old. Old enough for a great cocktail. Here's the recipe for a good one: Bot: [...]</code>

And our attempt to write the conversation in Inky looked like the following:

<code>- Hello! How are you? + I'm fine. -> good_mood + I don't want to talk. -> bad_mood = good_mood - What's your birthday? + 05/16/1976 - So you are 41 years old. Old enough for a great cocktail. Here's the recipe for a good one:- [...]-> END = bad_mood - Okay, I won't bother.-> END</code>

What did we do here? First, we let our bot ask for the user's mood (line 1) and then give him two choices, marked with the + symbol (lines 2 and 3). Ink allows us to use regular choices (marked with an asterisk) or sticky choices (marked with the plus symbol). We use sticky choices throughout our examples to allow the bot to jump back to a question while keeping all the choices. Otherwise Ink would remove a regular choice from the list after it was selected by the user. The arrows (->) are called diverts and allow us to branch the conversation by jumping to other parts of our script, called knots (starting with an equation mark, lines 5 and 12). So if the user is in a good mood, we let our bot proceed with the next question and ask for the user's birthday, otherwise we end the conversation.

Next, let's put the user's birthday and age in a variable and output the variables instead of fixed strings. Variables are defined through the VAR keyword and outputs through curly braces. (Ink does not let us start a choice text with a variable, therefore we have to prepend an escaped space there.)

<code>VAR birthday = 05/16/1976 VAR age = 41 […] - What's your birthday? + \ {birthday} - So you are {age} years old. […]</code>

Further, we can extend our script to output the phrase "Old enough for a great cocktail." only if the user is at least 18 years old. Conditional output can be achieved by placing a colon after the condition, followed by the output text, all wrapped in curly braces.

<code>VAR birthday = 05/16/1976 VAR age = 41 […] - So you are {age} years old. { age >= 18: Old enough for a great cocktail. Here's the recipe for a good one: } […]</code>

Up until now we used Ink's features to write a scripted conversation with user input in form of predefined choices, and we branched the conversation according to the choices made by the user. We wrote an Ink that delivers our sample conversation, but does not allow the user to input his real birthday and calculate his age. Now, what's missing is the possibility to input free text (needed for the user's birthday), to validate it and calculate the user’s age.

That's where things start to get a bit tricky, because Ink provides no mechanism for user input other than predefined choices, and Ink's internal functions are limited to simple boolean and numeric operations by design. But, Ink allows us to annotate our script using tags. Tags (starting with the hash symbol) can be used to add information to messages that are not displayed but used to describe what the game (or in our case, the bot) should do with the content. Have a look at the following tag we added to the birthday question to provide some metadata as a JSON string.

<code>[…] - What is your birthday? # { "userInteraction": { "type": "text", "stateVar": "birthday", "handler": "birthdayToAge", "validator": "date" }, "placeholder": "MM/DD/YYYY" } […]</code>

Now, Inky does not mind what we put in our tags, neither does it know what to do with this annotation we came up with. That's where we had to leave Inky and its preview and come up with our own implementation to display our Ink in the browser.

Enter InkJS and Angular

Due to our preexisting infrastructure for the client, we had already been committed to Angular and TypeScript. Luckily, there already is a JavaScript implementation of Ink called InkJS. It provides a simple API to retrieve the current messages, make choices and set variables, while it ensures the correct branching of dialogues.

I won't go into too much detail regarding our implementation. I recommend you to check out the code on Github to follow along.

Displaying our Conversations in the Browser

The core of our implementation is provided by the StoryService, wrapping the InkJS story object and exposing simple functions to start and stop a conversation, make choices and parse our custom annotations. To append some presentation logic, we wrap all messages provided by InkJS inside custom StoryPoint objects. Here is a quick overview of the possible properties of our story points:

  • sender: Appoints a sender to the story point (e.g. the bot or the user). The property is used to display the messages on the left or right side of the screen.
  • delay: Defines the delay by which the story point gets presented to the user, relative to the rendering of the previous story point. This is used to allow for a more realistic delay between the display of consecutive messages.

We can overwrite the default values of these properties by adding them to our annotation tags. Our MessagePanelComponent and MessageComponent handle the rendering and add some basic animations.

Extending Choices

In order to expand the user input and allow for inputs other than Ink's default choices, we introduced custom UserInteraction objects, which we construct from our annotations at runtime.

Let's run through our sample user interaction annotation from above:

  • type: Defines the input type we expect from the user. For now we only implemented the types text (presented as a simple text input field) and default (meaning Ink's default choices, presented as plain buttons). We use this information to determine which interface elements to show to the user and how to handle the input value.
  • stateVar: Refers to an Ink variable. We write the user input on this variable.
  • handler: Contains the name of a handler function defined in our Angular implementation. We use this functions to react to the user's input and execute our business logic (e.g. convert birthday to age).
  • validator: Contains the validator function name defined in our implementation. The function sets the validationError variable used in our Ink script, so we can branch the conversation accordingly (e.g. let the user correct his birthday input, if his previous input wasn't valid).
  • placeholder: Defines the placeholder text demonstrated to the user (in case of text input).

All fields are optional. We will maintain Ink's default choices if no custom user interaction is annotated. Presentation-wise we just pass our UserInteraction objects to our ActionBarComponent which recognizes how to render our two interaction types and how to deliver user input back to our service.

So, with all that, we are able to ask for user input not only in form of predefined choices but also free text, is checked for validity and passed on to custom handlers, and finally stored in an Ink variable for further usage in our conversation.

Feel free to play around with the conversation script located in the story.ink file. You can add your own validators to the UserInteractionValidatorService and implement your own handlers in the UserInteractionHandlerService. A demo of our result can be found here: https://sqrrl.github.io/angular-ink-chatbot/

Conclusion

Turns out that the unorthodox approach of using a markup language for text adventures brought some major advantages for the development of our chat bot prototyping architecture. Ink with its elaborate, yet easy to understand syntax and preview function, allowed us to write conversations that already suited a lot of our required features. Further, Ink came with a JavaScript runtime we could easily build upon to meet the missing requirements and bring our application to the browser. Best of all, by now we have quite a few non-tech people writing new chat bot conversations from scratch without needing any assistance from development.