-
Notifications
You must be signed in to change notification settings - Fork 5
New User Interview
This RiveScript Snippet will walk you through the steps needed to program a "New User Interview" into your bot.
This will make it so that your bot will ask a new user a series of questions (to get their name, age, gender, or so-on) the first time it chats with a new user. It also offers the user a way of escaping the interview, and a cancelation system in case the user is not complying with the interview process (after a few failed attempts at getting an answer, the bot exits the interview).
(This snippet was adapted from the AiChaos Aiden personality, so there may be some small errors to be cleaned up later).
The best way to test whether we've met a user before is to use the BEGIN block. This special "topic" in RiveScript is always tested first before the bot gets a reply for the user, and allows the bot to do "pre-processing" steps (like check and set variables) and "post-processing" steps (like uppercasing the entire reply, regardless of what the bot's reply is going to be).
By convention, the BEGIN Block in RiveScript goes into a file named begin.rive
, but you can put it anywhere. By following this convention you can know which file you put the BEGIN block into.
Start with this code:
! version = 2.0
// Some variables about the bot itself
! var name = Aiden
! var gender = male
! var age = 5
// This sets a local option to tell the parser what to do when we use a
// ^Continue command to extend a line of code across multiple lines.
// By default RiveScript just joins the lines together with no space in
// between, so this makes RiveScript join them by spaces instead:
! local concat = space
// Number of times to persistently ask the same interview question before
// giving up.
! global interview_retry = 3
We did a handful of things so far, so let's go over them one by one:
-
! version = 2.0
-- most RiveScript source files begin with this, and it just declares that the source file is using version2.0
of the RiveScript spec. -
! local concat = space
-- as the comments above this line indicate, this lets us be lazy with the^Continue
commands we'll use later so that we don't have to type out\s
to insert a space character. -
! global interview_retry = 3
-- this sets a global variable and it's how we make our interview configurable. This number will be used later to determine how many times the bot should try to get an answer to its next question before giving up.
And now we come to the BEGIN Block itself:
> begin
+ request
* <get isGroupChat> == true => {ok} // Don't do interview in group chats
* <get met> != true => <set met=true>{topic=interview1}{ok}
- {ok}
< begin
The request
trigger of the BEGIN Block is always tested first when the bot wants to look up a reply for the user. If the 'reply' given for this trigger contains the tag {ok}
, then RiveScript can continue to look up a reply and substitute the real reply for the {ok}
tag here.
I like to build in an "override" feature for skipping the interview process: if my bot can participate in group chats (such as a Slack channel or an IRC room), it would be very annoying for the bot to try and interview every person that interacts with it in such a public setting. So my chatbot program would set a user variable named isGroupChat
to a value of "true"
only when in a group chat, and "false"
when it knows it's chatting with a user one-on-one. (This variable would be set by the set_uservar()
function of the RiveScript library, but that's outside the scope of this tutorial).
The next condition is the important part:
* <get met> != true => <set met=true>{topic=interview1}{ok}
This looks up a user variable named "met" to see if it's "true"; a new user that the bot has never seen before won't have this variable set, so it would have a value of "undefined", which is not "true", and so this condition's reply is given.
It then sets the variable met=true
(so that on the user's next message, this condition is no longer true and you don't get caught in an infinite loop). Additionally, it puts the user into the topic interview1
and says {ok}
so that RiveScript will continue to find a reply for the user's message.
We now make a topic named interview1
(the topic we put a new user into just above).
// Interview: ask the user their name.
> topic interview1
+ *
- Hi there! My name is <bot name>. What's your name?
^ <set retry=0>{topic=interview2}
< topic
Since the user's first message could've been anything, we just have a catch-all wildcard trigger, *
, which matches anything the user could've said.
In response, the bot would say "Hi there! My name is bot name. What's your name?"
The bot puts the user into the topic named interview2
now, and resets the "retry counter" (the one we'll later compare to "interview_retry" so that the bot can give up on difficult users).
The interview can have as many questions as the bot author wants, but you'd find that a lot of the topics on your interview have common triggers. Instead of copying and pasting them around, you can put the common triggers into a shared topic that can be 'included' by the others.
Here is my common interview topic:
> topic interview_common
// User can say "continue" at any time to cancel out of the interview.
+ [*] (continue|cancel|stop|quit) [*]
- Okay, let's just chat then. What's up?{topic=random}
+ int abort
- Nevermind then. What's up?{topic=random}
+ int help *
- If you don't want to tell me your <star>, just type "continue".
- Or just type "continue" if you don't want to tell me your <star>.
< topic
This sets up a trigger that allows the user to say words like "continue", "cancel", "stop", or "quit" to abandon the interview at any time. This way I don't have to copy the "escape trigger" into every topic of the interview.
I also define a couple of "internal" triggers: I tag these with the word "int" for "internal". These are technically normal triggers, and the user could match them if he knew how they were spelled, but they're mostly just here to redirect to from other triggers for internal program logic.
Now let's move on to letting the user actually give an answer to our first question.
// Interview: user responds with their name. Try 3 times to get their answer
// before giving up.
> topic interview2 includes interview_common
+ * *
* <get retry> > <env interview_retry> => {@int giveup}
- I only want your first name. {@int help}
- Just tell me your first name. {@int help}
- Can I just get your first name? {@int help}
+ *
- <set name=<formal>>Your name is <get name>? Cool! Are you a boy or a girl?
^ <set retry=0>{topic=interview3}
+ (my name is|call me|i am|im) *
@ <star2>
+ int giveup
- <set name=<id>>I'll just call you <get name>. Anyway, what's up?
^ {topic=random}
+ int help
- <add retry=1>{@int help name}
< topic
There's a lot going on with this topic.
This topic is interview2
(the one we move the user to after we asked their name, so that they can answer), and it includes the topic interview_common
that we defined above. This means that the full set of triggers under interview2
and those under interview_common
will behave as though they were all defined under the same topic together. This is a powerful feature of RiveScript to help organize common triggers among many topics.
The first trigger was * *
: if the user sent multiple words in their message, the first word would be caught by <star1>
and the remainder go into <star2>
. We catch this in order to make sure the user only gave us one name, not a full name or some other sentence instead.
A lot of the triggers compare the retry
variable of the user to the global interview_retry
limiter variable (which is 3 in this example). Every time the user fails to answer the question appropriately, we do <add retry=1>
to increment the counter. So if the user fails to answer the question 3 times, we can give up and put them back in the default topic ("random"
).
When we've successfully gotten the user's name, we then ask their gender.
// Synonyms for gender pronouns that we can use later
! array malenoun = male guy boy dude boi man men gentleman gentlemen
! array femalenoun = female girl chick woman women lady babe
// Interview: user responds with their gender.
> topic interview3 includes interview_common
+ @malenoun
* <bot gender> == male => Me too! How old are you?{@int male}
* <bot gender> == female => Cool! I'm a girl. How old are you?{@int male}
- Cool! I'm a <bot gender>. How old are you?{@int male}
+ @femalenoun
* <bot gender> == female => Me too! How old are you?{@int female}
* <bot gender> == male => Cool! I'm a boy. How old are you?{@int female}
- Cool! I'm a <bot gender>. How old are you?{@int female}
+ [i am|im] a (@malenoun|@femalenoun)
@ <star>
+ *
* <get retry> > <env interview_retry> => {@int abort}
- I asked if you're a boy or a girl? {@int help}
- So are you a boy or a girl? {@int help}
+ int help
- <add retry=1>{@int help gender}
+ int male
- <set gender=male><set retry=0>{topic=interview4}
+ int female
- <set gender=female><set retry=0>{topic=interview4}
< topic
// Interview: user responds with their age.
> topic interview4 includes interview_common
+ #
- <set age=<star>>I'll remember that you're <get age> years old.
^ I'm <bot age>, myself. That's all the questions I have for now -- let's
^ chat. What's up?{topic=random}
+ (i am|im) # [years old]
@ <star2>
+ *
* <get retry> > <env interview_retry> => {@int abort}
- Tell me your age as a number. {@int help}
- Can you tell me your age as a number? {@int help}
+ int help
- <add retry=1>{@int help age}
< topic
This shows a few more examples of questions to ask.
For the gender question, I used arrays for gender pronouns so that I could use those in my triggers instead of wildcards. This way I can constrain the set of possible messages the user can say that will match these triggers.
After that, the bot asks the user for their age, and once it's been answered the interview ends. You could follow this pattern and continue adding more questions to your interview.