With its mission statement: “to organise the world’s information and make it universally accessible and useful.” Google invests a lot of time and money in understanding human language. No surprise that Google Dialogflow provides one of the best Natural Language Understanding (NLU) services and has become the go-to platform for building smart chatbots and voice assistants.

Creating robust dialogs with Dialogflow can be a challenge though. Using followup intents or context, you can easily create flows, but they can be brittle or unpredictable depending on how you set context lifespan and implement fallbacks. Making parameters required triggers Dialogflow to perform slot-filling, prompting the user for missing entities. This is a closed (endless) dialog loop where your users may get stuck. As a creator, there is no easy way to change this behavior without resorting to writing Javascript code.

This article discusses what makes good dialogs and what the challenges are implementing them with Dialogflow. We provide an alternative to building dialog flows, making it easier to implement robust dialogs using a scripting language called BubbleScript that integrates with Dialogflow.

What makes good dialogs

When we say better dialogs, what do we mean with that? Of course, good dialogs start with well-designed flows and good copy. What we are referring to here though, are the more technical properties of the dialogs.

Good dialogs should be discoverable, resilient, and progressive.

Let me explain

Discoverable

Dialogs should implement ways for users to find out where they are in the conversation and what the expected or potential next steps are. Now the easiest way to implement this is to make a very rigid flow with quick replies that don’t allow users to wander off track. This would make the conversation very robotic and unnatural.

When we allow for a more free-flowing conversation, we need to provide hints to the user on how to progress in the form of re-prompting after a short sidetrack. Also, it should tell users where they are in a dialog by summarizing what is understood so far, especially before committing to a goal or performing a task.

This requires the dialog (manager) to self reflect on current conversation state, goal, and user-provided facts.

Resilient

When we design our chatbots dialog flows, we often start with the ‘happy path’, providing a generic fallback: “I didn’t get that. Can you say it again?”. Besides the fact that this may cause your users to try again and fail again (and getting frustrated), there may also be other side effects. In essence, your chatbot loses track of what it was doing (goal set) and engages in a conversation going nowhere.

A simple “hello” in the middle of a conversation puts many chatbots off track.

With resilience, we mean that your chatbot stays goal focussed, steering users back to a happy path situation or at least providing means for users to find their way back. In a previous article, we discussed this using a progressive fallback.

Progressive

Computers are extremely good at repeating themselves, and so are many chatbots when prompting users for information. Humans, on the other hand, are not that patient: most bail out after 3 tries.

With progressive prompting, we mean that every re-prompt is a bit more explicit. After 3 turns, it allows the user to achieve its goal by other means like navigating available topics or a handover to a live agent.

So how do we implement such properties in Dialogflow?

Simple example

Let’s take a straightforward example: you want users to be able to register for updates on your products or services. A simple flow graph would look like this:

This diagram has an initial prompt asking the user how it can be of service. In this case, only one service is available: registering.

In practice, you will see users responding with a variety of valid responses you may not have anticipated: (“don’t have an email address”) and chit chat like (“you rock!”).

Let’s implement a few of these cases as well.

Building flows with Dialogflow contexts

In Dialogflow, you don’t implement strict flows like in the design but instead, model it using intents and context. This allows for a more free-flowing dialog, BUT also means your chatbot may lose context and get off track.

Implementing the “register” intent with email capturing and re-prompting is the most exciting part of this flow. Let’s see how we would implement this in Dialogflow:

We would first create an intent that is triggered by “register me” (or similar phrases) and prompts the user for their email address. To set a context for the intents firing as a response, you set an output context “email” with a lifespan of 1 (following best practice). Next, we create intents for the replies we expect the user to give in this context, firstly handing the happy path of the user responding with a valid email address (intent: register.email). This intent uses the sys.email entity to capture the user’s email and store it in the context parameter $email and replies to the user with “Thanks”.

Now, to handle the case of users not providing an email address, we implement a contextual fallback, only triggering when the context “email” (green) is set. To implement a form of progressive prompting, we create two more fallbacks chained using context where the last fallback clears the context, effectively returning the user to the initial context.

Next to these generic fallbacks, we can implement some edge-cases we expect users to reply with. Replies like “I don’t know my email” or “I won’t provide my email” would be possible cases to stop re-prompting.

This is what list of intents looks like:

Because naming is all you can use to organize your intents, it’s important to name them in a way you can easily find intents that belong together. You name the intent after what it triggers on (the user’s intent), not after the bot’s response.

Challenges

Though the interplay between intents and context seems simple, it does pose some challenges:

Digression leads to losing context

Note there is a general intent called “smalltalk.hi” that has no context. It seems harmless, but when we say “Hi” where the chatbot expected an email address, it gets off track: The bot says “Hi” back, but loses the “email” context (given we used a lifespan of 1). Effectively this means that if the user types a valid email address after this point, it won’t trigger the register.email intent but rather the general fallback intent.

As it turns out this is not an easy issue to fix: you can set the lifespan to a higher number but this brings a new set of issues especially when having deeper nested contexts. You can also give the “hi” intent an input and output context, scoping it to the “email” context. This would mean you need to duplicate every general intent.

The same prompt is spread across intents

Another issue is to do with the re-prompting, or more generally, the fact you cannot reuse responses across intents. A good example is the “What can I do for you”. When we exit the “email” scope, we want to prompt the user with a generic response to indicate we are back at the start and awaiting new orders.

At this point, you might start to investigate a webhook approach, effectively managing content and flow (context) outside of Dialogflow. Even though this may start simple, in our experience you quickly find yourself knee-deep in Javascript and technical details.

Perhaps this is an excellent moment to look into BubbleScript.

Building flows with BubbleScript

We developed BubbleScript for precisely this reason: we needed to build chatbots for our clients and found that using existing platforms didn’t scale in terms of workflow and maintainability. You see many platforms using a visual flow editor similar to the flowchart design. But like the flowchart design, it works for happy paths BUT starts to break down when you build larger chatbots and have to make changes at scale.

When these platforms offer you a way to augment the nodes using Javascript, you effectively get the worst of both worlds. As a designer, you still need to think about the code (which most cannot easily comprehend), and as a developer, you often find your code broken by changes to the flow, and you quickly become a bottleneck in the process.

With BubbleScript, we aim to bring the designers and developers on the same page: the language is easy enough for designers to understand and reason about, and powerful enough for developers to create sophisticated reusable blueprints.

The same flow, including edge cases implemented in BubbleScript, would look like this:

dialog main do
  say "Welcome! 👋"
end

dialog __root__ do
  prompt "What can I do for you?"

  dialog trigger: @register, label: "register", do: invoke register
end

dialog register when user.email do
  say "Thanks!"
  say "I will keep you updated on #{user.email}"
end

dialog register do

  ask ["What is your email address?","your email address?"], expecting: [@email]
  branch answer do
    :continue ->
      say "Without an email address I cannot continue."
      invoke cancel
    answer == @email  ->
      user.email = answer.intent.entities.email
  end
  invoke register

  dialog cancel, trigger: @cancel do
    ask "Do you want to abort?", expecting: [@yes, @no]
    branch answer do
      @yes ->
        reset
      @no ->
        say "Ok lets try again"
        invoke register
    end
  end

  dialog __unknown__ do
    once do
      say "I didn't get that"
      say "Sorry still don't understand"
      say "Ok last time"
    after
      continue
    end
  end

end

Let me walk you through it: when the user interacts with the chatbot for the first time, the dialog “main” (or actually __main__) is invoked by the platform. Here we say “Welcome” to the user. There are a handful of simple statements you can use inside dialogs to interact with users: say, ask, and show.

Next, there are no more statements to perform (the call stack is empty), so the platform calls the dialog __root__, which prompts the user with “What can I do for you?” awaiting a reply.

When the user says: “register me” the dialog manager searches for the first dialog that matches. In this case, the dialog named register with a triggering intent @register. This dialog invokes the dialog email.

Triggers can be either rules-based, event-based, or intent-based and are resolved using a bubbling algorithm first searching the current ask/prompt expectation (left-to-right) and next: the inner dialogs bubbling up to the next scope (top-down).

To show all the moving parts: the intent declarations are also displayed here so you can see how they integrate with the Dialogflow intents and entities. These can also be managed in the platform directly via a GUI.

@yes            intent(dialogflow: "defaults_yes", label: "Yes")
@no             intent(dialogflow: "defaults_no" , label: "No")
@cancel         intent(dialogflow: "defaults_cancel")
@register       intent(dialogflow: "register")
@email          intent(dialogflow: "email")

The email dialog asks the user for his/her email address, expecting to get an email intent with an entity email. When it doesn’t find this intent, it bubbles up and searches for dialog triggers that match the user’s reply. For instance, the cancel intent could match. If not, it searches the root scope and finally calls __unknown__ following the same bubbling pattern.

Here we implemented the progressive prompting; the statements within the once block are called successively: every time we enter the block, the next one is selected until we hit the after block where we call continue.

dialog __unknown__ do
  once do
    say "I didn't get that"
    say "Sorry still don't understand"
    say "Ok last time"
  after
    continue
  end
end

After 3 unmatched replies, the continue statement continues the awaiting ask where we can branch on the answer the user has given.

  branch do

  answer == :continue ->
    say "Without an email address I cannot continue."
    invoke cancel

  answer == @email  ->
    user.email = answer.intent.entities.email

  end

The happy path is where the user-provided an email address in which case we extract the email entity from the answer and set it as a property of the current user. Next, we invoke ourselves - invoke register - but give this time it already has an email set it triggers the dialog right above it:

dialog register when user.email do
  say "Thanks!"
  say "I will keep you updated on #{user.email}"
end

With when, you can put ‘guards’ on a dialog: only when the expression that follows is true will the dialog be triggered. In this case, it handles the fact that if the user email is already known, it won’t for the email.

In case we continued without an answer it will inform the user it cannot continue without an email address and invokes a dialog that triggers on “cancel”.

dialog cancel, trigger: @cancel do
  ask "Do you want to abort?", expecting: [@yes, @no]
  branch answer do
    @yes ->
      reset
    @no ->
      say "Ok lets try again"
      invoke register
  end
end

The inner dialog cancel with a trigger on the intent @cancel fires and asks the user whether to continue or abort. In case the user chooses to abort, the call stack is reset, and __root__ is invoked again by the platform asking the user: “What can I do for you”.

In this small example, we have covered almost everything there is to know about BubbleScript.

Now let’s see how it compares to the Dialogflow implementation and how you can reuse your existing Dialogflow agent and extend it with BubbleScript based dialogs.

Conclusion

Compared to Dialogflow, BubbleScript gives the creator a lot more control over the behavior of dialogs and flows. You can use your existing agent with trained intents to gradually weave BubbleScript dialogs into the conversation.

With prompt your dialogs are automatically discoverable when you give them a label. The full state is available to introspect and use across sessions (automatically saved and restored)

The bubbling behavior of BubbleScript makes it easy to overlay edge cases on top of general cases allowing for modular and flexible flow designs.

The BubbleScript dialog manager keeps stacked state frames meaning it can deal with unforeseen input and return to earlier dialogs in progress. It makes dialogs a lot more resilient.

Using statements like once or when, you can quickly implement progressive dialogs that prompt users more explicitly and exhaust after 3 turns.

In short: You can use BubbleScript with your existing Dialogflow agent to make better, more maintainable dialogs.

Sign up and read the documentation and try the above BubbleScript code

Anne Bakker

Anne Bakker

Co-founder Botsquad

email me