DEV Community

Troels F. Rønnow
Troels F. Rønnow

Posted on

Context-aware chatbots using Wonop

In this tutorial, we will build a small context-aware bot which will serve as a virtual assistant for booking cars, hotels and dinner tables. We will be using https://wonop.com to do so. Through these tutorials, you will be introduced to the different concepts of Rocklang one by one. By the end of the tutorial, you will be able to build your own context-aware bots. Here is what the end result will look like:

Alt Text

Traditional Hello World

To get familiar with rocklang we create a traditional Hello world example. Navigate to the Agents in the GUI and, if you have not already, create a new agent. Once created, you will see the code editor. Enter the following snippet:

fn main(phrase: String): Int32
{
  // Making the agent say a phrase
  say("You said: "+ phrase)

  // Printing output to the console
  printLn("Debugging: "+ phrase);

  return 0i32;
}
Enter fullscreen mode Exit fullscreen mode

The main function is the one which is invoked when you run the program. This simple script demonstrates two ways of creating output using Rocklang. The first function say is used to get the agent to respond to the phrase. The second function printLn prints a statement to the console at the bottom of your screen.

Creating Welcoming Context

The previous script replies You said: ... and does not differentiate between different intents. The first thing for us to do is to try to understand the users intent. To this end, we will make use of Rocklang context, pattern and route concepts.

We will start off by a simple bot that responds to the phrase "hello world" and for any other phrase says "I do not understand what you are saying.".

context Lobby {
  // Defining the pattern we want to match against
  pattern Hello: "hello" "world";

  // Creating a route for the pattern, meaning
  // that a match on "Hello" will invoke the function
  // "hello".
  route Hello: hello;
}

fn hello(ctx: MatchContext): Bool
{
  // Answers to the matched route
  say("Well, hello there!");

  // Informs the routing system that the route
  // indeed was successful by returning true.
  return true;
}

fn main(phrase: String): Int32
{
  // Getting the current user context. If no context
  // is found, then we default to "Lobby".
  let ctx: UserContext = getUserContext("Lobby");

  // Attempting to route the input phrase within the user
  // context
  if(!routePhrase(ctx, phrase))
  {
    say("I do not understand what you are saying.");
  }

  return 0i32;
}
Enter fullscreen mode Exit fullscreen mode

A lot of things are going on in the above example. If we start with the main function: Here we first get the user context. If no user context exists yet, we default to "Lobby". This means that "Lobby" will be the first context within which we will try to match the user's request. Next, we ask the system to route the phrase within the context. In our case, the context only contains one pattern, namely "hello" "world" which means that matches will only be found for "hello world" and anything else will fail at being routed. If routing fails, the agent informs that the query is not understood.

Extracting Contextual Information

If you are familiar with Regex, you will note that context patterns have taken inspiration from this concept. We illustrate this by making a small adjustment to our previous agent:

context Lobby {
  // Defining the pattern we want to match against
  pattern Hello: Exclamation ("world"|"earth"|"planet");

  // ...
}
Enter fullscreen mode Exit fullscreen mode

This relatively small change to the program has a huge impact: The agent will now respond to phrases like "Hello world", "hey planet" and "cheers earth". Note how little we had to do to cover this wide range of possibilities.

As we are creating a Lobby we would typically expect a greeting either followed by a name or nothing. We would like to extract the name from the phrase to check if the user got the name right or not. To do this, we update the pattern to:

context Lobby {
  // Defining the pattern we want to match against
  pattern Hello: Exclamation name=AnyWord?;

  // ...
}
Enter fullscreen mode Exit fullscreen mode

And then we create the logic to deal with the hello request:

fn hello(ctx: MatchContext): Bool
{
  let howCanIHelp : String = "How can I help you?";

  // Checking whether the user used a name
  if(ctx.has("name"))
  {
    // Extracting the name from the context
    let name: String = toString(ctx.get("name"));

    if(name == "lucy")
    {
      // The bot acts happy that you recognised its name
      say("Hi! Good to see you!");
    }
    else
    {
      // Informing the user that he/she got the name wrong
      say("My name is not " + name + ". My name is Lucy");
      howCanIHelp = "Anyway, how can I help you?";
    }
  }
  else
  {
    // In case that the user did not use a name, we give neutral response
    say("Hello there!");
  }

  // Finally, we ask how we can help
  say(howCanIHelp);

  return true;
}

Enter fullscreen mode Exit fullscreen mode

The logic is relatively straight forward: Either the user used a name or not. If a name was used we check that it is the correct name. If not, we inform the user that the name was wrong and otherwise we greet back.

Advanced Patterns

As you try to match more advanced scenarios, the patterns you will use become longer and more involved. To keep things simple and testable, you can break patterns down into subpatterns. As we are building a virtual assistant for bookings, we need to be able to match the many different ways a user may formulate this request. We do this by breaking the sentence to parts. We first define the introductory part of the sentence, then the part with the active verb and finally, any modifications to the sentence:

context Lobby {
  // ...

  // Introductory part of the sentence
  pattern IWouldLike: "i" "would" Adverb* ("like" "you"?|"want" "you") "to";
  pattern CouldYou: "could" "you";
  pattern INeed: "i" ("need"|"want") "you"? "to";

  // The active verb
  pattern Booking: (IWouldLike|CouldYou|INeed) ("book"|"reserve"|"get"|"find")?;

  // Phrase modification
  pattern ForWhoOrWhat: "for" forWhoOrWhat=(Determiner (Pronoun|Noun));

  // The full phrase and the route
  pattern ObjectBooking: Booking determiner=Determiner what=(Noun+) ForWhoOrWhat?;
  route ObjectBooking: objectBooking;
}
Enter fullscreen mode Exit fullscreen mode

Note how we are nesting patterns to break the problem down into small conquerable parts. This pattern will match a wide variety of phrases ranging from requiring ("I want you to book a hotel") through wishful asking ("I would want you to book a hotel") to politely requesting ("I would like to book a car").

Navigating Contexts

Next, we craft the logic to handle requests made with this pattern.

fn objectBooking(ctx: MatchContext): Bool
{
  // Defining the variables we will need
  let what: String = toString(ctx.get("what"));
  let determiner: String = toString(ctx.get("determiner"));
  let newContext: String  = what.capitalize() + "Booking";
  let who: String;

  // Checking if the context exists
  if(hasContext(newContext))
  {
    // If the request had a modification, we give a response
    // to reflect that we understood the request
    if(ctx.has("forWhoOrWhat"))
    {
      who = toString(ctx.get("forWhoOrWhat"));
      say("So you want to book " + determiner + " " + what + " for " + who);
    }
    else
    {
      // Otherwise, it's a straight forward request
      say("So you want to book " + determiner + " " + what);
    }

    // Regardless, we inform the user that we can do this
    say("Sure, I can do that!");

    // And then we change context
    pushContext(newContext);
  }
  else
  {
    // We do not have a context to handle this case and we inform the user about this
    say("Unfortunately, I don't know how to get you " + determiner + " " + what);
  }

  return true;
}
Enter fullscreen mode Exit fullscreen mode

Most of the above logic is similar to the logic we did for the greeting section. However, we do use two new functions hasContext and pushContext. The function hasContext checks whether the developer has created a context with a specific name and returns a boolean to reflect that. The function pushContext pushes the user into a new context with new patterns. If this happens are patterns in the Lobby are put aside to the benefit of the patterns in the new context.

The final step in this tutorial is to create dummy contexts for cars and hotels. To do this, we add a simple context with one route: The main pattern matches any phrase and invokes the route function:

context CarBooking
{
  pattern WhatCar: AnyWord*;
  route WhatCar: whatCar;
}

fn whatCar(ctx: MatchContext): Bool
{
  // User feedback
  say("Car booked!");

  // Popping the context to return back to the Lobby
  popContext();

  // Informing the routing system that we were successful
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Inside the route function, we call popContext to return back to the Lobby context. Finally, we can now extend this by adding similar contexts for the hotel:

context HotelBooking
{
  pattern WhatHotel: AnyWord*;
  route WhatHotel: whatHotel;
}

fn whatHotel(ctx: MatchContext): Bool
{
  say("Hotel booked!");
  popContext();
  return true;
}
Enter fullscreen mode Exit fullscreen mode

In just about 140 lines of code (including comments), we have made a context-aware agent that has the capability of understanding what you want to do and navigate to that context. Of course, there is more work to be done in order for this to become fully functional and we will cover this in the upcoming tutorials.

I would love to hear some feedback on this language I've developed! Finally, you can join our community:

https://wonop.com/
Dev.to: @wonop
Discord: https://discord.gg/YtkWN9fb
Twitter: https://twitter.com/wonop_io

Top comments (0)