chatbot_utils
Table of Contents
Chatbot utils provides easy-to-use tools for building a chatbot capable of returning flexible, contextual responses when provided with text input.
Supports Python 2.x and 3.x.
By Contextual responses, I mean something like this;
human >> hey, what time is it? bot >> it's 10.32pm human >> is that past my bedtime? bot >> no, you're good
The second phrase typed by the human, "is that past my bedtime?"
, is
ambiguous, and required the bot to understand that this was an incomplete
question related to the previous question, i.e. the context.
Installation
From PyPi
pip install chatbot_utils
From Github
git clone github.com/eriknyquist/chatbot_utils
cd chatbot_utils
python setup.py build
python setup.py install
API documentation
Read the API documentation here
Example bot with chatbot_utils, showing how to use contexts
The following example shows how to create a bot that can provide contexual responses to specific questions:
import random
import time
from chatbot_utils.responder import Responder
from chatbot_utils.utils import ContextCreator, get_input
random.seed(time.time())
responder = Responder()
# Add a context for talking about cats
with ContextCreator(responder) as ctx:
# Phrase to trigger entry into cat context
ctx.add_entry_phrases((["(.* )?(talk about|tell( me)? about) cats?.*"], ["Sure, I love cats"]))
# These phrases will only be recognized after the entry phrase has been seen
ctx.add_responses(
(["(.* )?favou?rite thing about (them|cats?).*"], ["They are fuzzy"]),
(["(.* )?(do )?you have (one|(a )?cat).*"], ["No, computer programs can't have cats."])
)
# Add a nested context inside the cat context (you can do this as deep as you like)
with ContextCreator(ctx) as subctx:
# Phrase to trigger entry into cat food context, will only be recognized when we're already in the cat context
subctx.add_entry_phrases((["(.* )?(talk about|tell( me)? about) food?.*"], ["Sure, let's talk about cat food"]))
# These phrases will only be recognized after BOTH entry phrases have been seen
subctx.add_responses(
(["(.* )?(favou?rite|best) type( of food)?.*"], ["Computer programs do not eat cat food."]),
)
# Add explicit exit phrase for cat food subcontext (if no exit phrase is added,
# then he only way to exit the context is using a phrase that was added to the top-level
# responder object with Responder.add_response())
subctx.add_exit_phrases((["(.* )?(stop talking about ((dog )?food|this)|talk about something else).*"], ["OK, no more dog food talk."]))
# Add a context for talking about dogs
with ContextCreator(responder) as ctx:
# Phrase to trigger entry into dog context
ctx.add_entry_phrases((["(.* )?(talk about|tell( me)? about) dogs?.*"], ["Sure, I think dogs are great"]))
# These phrases will only be recognized after the entry phrase has been seen
ctx.add_responses(
(["(.* )?favou?rite thing about (them|dogs?).*"], ["They are loyal"]),
(["(.* )?(do )?you have (one|(a )?dog).*"], ["No, computer programs can't have dogs."])
)
# Add a nested context inside the dog context (you can do this as deep as you like)
with ContextCreator(ctx) as subctx:
# Phrase to trigger entry into dog food context, will only be recognized when we're already in the dog context
subctx.add_entry_phrases((["(.* )?(talk about|tell( me)? about) food?.*"], ["Sure, let's talk about dog food"]))
# These phrases will only be recognized after BOTH entry phrases have been seen
subctx.add_responses(
(["(.* )?(favou?rite|best) type( of food)?.*"], ["Computer programs do not eat dog food."]),
)
# One of these responses will be randomly chosen whenever an unrecognized phrase is seen
responder.add_default_response(["Oh, really?", "Mmhmm.", "Indeed.", "How fascinating."])
# These phrases will only be recognized when no context is active
responder.add_responses(
(["(.* )?hello.*"], ["How do you do?", "Hello!", "Oh, hi."]),
(["(. *)?(good)?bye.*"], ["Alright then, goodbye.", "See ya.", "Bye."])
)
# Simple prompt to get input from command line and pass to responder
while True:
text = get_input(" > ")
resp, groups = responder.get_response(text)
print("\n\"%s\"\n" % (random.choice(resp)))
Save this file as example_bot.py
and run it with python example_bot.py
.
Example output:
#~$ python example_bot.py > hello! "Hello!" > hey, can we talk about dogs for a bit? "Sure, I think dogs are great" > what's your favourite thing about them? "They are loyal" > do you have one? "No, computer programs can't have dogs." > OK, let's talk about cats now "Sure, I love cats" > do you have one? "No, computer programs can't have cats." > and what's your favourite thing about them? "They are fuzzy"
Example bot with chatbot_utils, showing how to use format tokens
The following example shows how to create a bot that can remember what you said your favourite movie was, ad report it back later when asked:
from chatbot_utils.responder import Responder
from chatbot_utils.utils import ContextCreator, get_input
responder = Responder()
responder.add_default_response("Please tell me what your favourite movie is")
responder.add_responses(
# When the bot is told what my favourite film is, it will save whatever film I said (4th
# parenthesis group, or p3) in a new variable named "faveMovie"
(["(.* )?(favou?rite|fave) (movie|film) is (.*)$"],
"Cool, I will remember that your favourite film is {p3}!;;faveMovie={p3}"),
# When the bot is asked to recall what my favourite film is, it will report the value of 'faveMovie'
(["(.*)?(what is|what'?s|(can you )?tell me )?(what('?s)? )?my (fave|favou?rite) (movie|film).*"],
"Your favourite film is {faveMovie}!")
)
# Simple prompt to get input from command line and pass to responder
while True:
text = get_input(" > ")
resp, groups = responder.get_response(text)
print("\n\"%s\"\n" % resp)
Save this file as example_bot.py
and run it with python example_bot.py
.
Example output:
#~$ python example_bot.py > howdy! "Please tell me what your favourite movie is" > hmm, OK, I guess my favourite film is Gone With The Wind "Cool, I will remember that your favourite film is Gone With The Wind!" > hey, can you tell me what my fave movie is? "Your favourite film is Gone With The Wind!" > alright, now my favorite movie is spiderman 2 "Cool, I will remember that your favourite film is spiderman 2!" > what's my favourite film? "Your favourite film is spiderman 2!" >
Performance characterizations
A core component of chatbot_utils
is a custom dictionary called a ReDict,
which expects values to be set with regular expressions as keys. Values can then
be retrieved from the dict by providing input text as the key, and any values
with a matching associated regular expression will be returned.
ReDicts with a large number of regular expressions (for example, a Responder
with several thousand pattern/response pairs added using the add_response
method) may take a significant amount of time when compiling the regular
expression(s) initially. By default, this is done automatically on first
attempt to access a ReDict, but you can also call Responder.compile()
explicitly to control when the regular expressions associated with a responder
are compiled.
One additional quirk to note is that having more parenthesis groups in your regular expressions results in a significant increase in compile time for ReDicts with a large number of items.
Analysis: compile time & fetch time with 100k items, no parenthesis groups
Each regular expression in the 100k items of test data used for this analysis was 14-19 characters in length, used several common special characters and was of the following form:
foo? 10|bar* 10
The Time to compile was calculated simply by timing the ReDict.compile()
method. The Time to fetch is an average calculated by randomly fetching 10% of
the total number of items in the dict (e.g. for a dict with 1000 pattern/value
pairs added, 100 randomly-selected items would be fetched).
Analysis: compile time & fetch time with 100k items, extra parenthesis groups
Each regular expression in the 100k items of test data used for this analysis was at least 25-30 characters in length, used several common special characters and was of the following form (note the addition parenthesis groups):
(f)(o)o? 10|b((a)(r)*) 10
Same as the previous test, the Time to compile was calculated by timing the
ReDict.compile()
method, and the Time to fetch is an average calculated by
randomly fetching 10% of the total number of items in the dict.