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:
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;
}
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;
}
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");
// ...
}
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?;
// ...
}
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;
}
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;
}
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;
}
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;
}
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;
}
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)