Loan Wrangler is a friendly chatbot, ready to help you with (almost) anything involving borrowing and using the library’s awesome tools and media equipment.
Libraries are awesome because they want you to have what you want, when you want it, for free. This started with books, then digital resources, and now the possibilities are endless! For example, the Olin College library wants users to be able to borrow tools and media equipment. Since the current loan system wasn’t made for tools (which are different than books in a few key ways), we created a chatbot that can help library patrons check out, use, and return tools in a much more flexible, friendly, and fun way.
Since the bot is still under development and not yet public, you will need to chat with it from the Library’s facebook account. To be able to message it yourself, you will need to be added to the page’s admins by the Library.
When you message Loan Wrangler, it will do its best to meet all your tool and media equipment loan needs. Since it is still a young bot, you will notice it doesn't have answers for everything! Here is an example conversation you might have:
At this point the bot would send a message to the user with the drill checked out, like "someone's looking for the drill, could you return it if you're done using it?" Once the drill is returned, the next user can check it out:
Shortly before the drill is due, the bot will send a reminder, and keep sending periodic reminders until the drill is returned.
When the user returns the drill, they can let the bot know it's back:
A user can also check out more than one tool at a time, and ask for help with tools:
Likewise, a user can return more than one tool at a time:
If a user asks for help that Loan Wrangler isn't equipped to give, it redirects their question to the librarians (note that this feature is only partially complete).
Currently, the bot can:
- Check out an item to a user
- Accept a returned item
- List some tools available
- Send reminders to a user when a tool is nearly due
- Indicate whether a desired item is available
- Ask a user to return an item at the request of another user
- Give resources to help a user with an item, or send an email to the librarians when it doesn’t know how to help
We’ve been tracking bugs and new feature ideas on the GitHub issues page.
The return reminders are currently done using a threaded interval timer. There’s a current bug where the messages are sent twice instead of once, usually one second apart. We spent a lot of time looking at it and have been unable to figure out why.
1. Either install a local MongoDB instance, or provision a hosted instance and set mongo_uri
to its URL.
macOS running Homebrew: run brew install mongodb
, and then following the instructions to start the instance.
Ubuntu: follow these instructions to install and run a local instance.
Hosted instance (for example, mLab): Set the mongo_uri
envirnonment variable to a MongoDB connection string URI to the instance.
2. Install Python package dependencies: pip install -r requirements.txt
. If you want to update the documentation, you will also need to run pip install -r requirements-dev.txt
. Also, this project is written in Python 3, so if you’re not using a virtual environment, you should run those commands instead with sudo pip3
.
3. Run python3 create_tools_from_tind.py
.
4. Set the following environment variables:
Our code has the following app secrets:
validationToken
is a Facebook variable that confirms for Facebook that you own the app that you say you own. You can set this to anything for local development.pageAccessToken
is generated by Facebook for your page. You can set this to anything for local development.
Our code contains four primary files:
server.py
is the main application with the Flask routing structure. It creates instances of the next three objects.messenger_client.py
defines a class for interfacing with the sending and receiving of Facebook messages.database_client.py
defines a class for interacting with the Mongo database, and includes functions that get and set the various information we care about.conversation_handler.py
is the primary program for parsing received message text and determining the appropriate response. It takes a database client in its constructor since it needs to get tool and user information.
We also have some additional files:
create_dummy_tools.py
populates the database with a dummy set tools of tools, for testing. This is currently obsolete.create_tools_from_tind.py
populates the tools database by pulling an XML from Tind and parsing it. This is a separate script from our app, so it only executes when it is explicitly run. Future work could including making this update live.send_email_to_librarian.py
is imported and used to send an email when needed.tests.py
contains our tests. The test coverage is low and non-ideal.fake_database_client.py
creates a fake database client that returns fake data, to be used in the tests.
We use user stages to keep track of where a user is in their conversation, and use that information to decide what they mean when we receive a message that just says “yes”, for example. The stages are:
self.NO_CONTACT = 0
self.SENT_GREETING = 1
self.WANT_CHECKOUT = 2
self.CONFIRM_TOOL = 4
self.HOW_LONG = 5
self.CLOSING = 6
self.WANT_RETURN = 7
self.CONFIRM_TOOL_RETURN = 8
self.AVAILABILITY_QUESTION = 9
self.SEND_LIST = 10
Additionally, our conversation handling works through “dumb” searching for keywords in user messages. It does not (yet) use machine learning. That’d be a pretty good idea though. Currently, when parsing for words, we look for: checkout words, return words, closing words, availability words (asking if a tool is available), and help words.
When adding to these word lists, keep in mind that the bot looks for that word anywhere in the user message, so choose them with care. For example, at an earlier point in the project, we included the word “in” as an availability word, for the case of a user asking “is the camera in?” The problem this presents is that it would see “in” in other words as well (for example, “I am checking out a camera”), which would be misinterpreted because the word “checking” also contains the word “in”.
The determine_response_for_user
function is the main part of this, and follows a mostly predictable structure. It’s pretty much a list of if statements that determine a response based on user stage. There are a couple important exceptions.
The end of that function should never be reached, and if it is, it’s a bug. It should be structured in a way that it returns before ever getting to the end of the function.
The first chunk of the function is structured differently. It looks like this (pseudo-code):
if the message is a closing:
Return a farewell
if the message is asking for help:
Return a help resource
if the user stage is SEND_LIST
If the message is “view more” then return a response
Otherwise, don’t return because it needs to be treated like NO_CONTACT
if the user stage is NO_CONTACT
Check for return words and set user stage
Check for availability words and return response
Check for checkout words and set user stage
Else, return that we don’t understand
// other if-checks for user stages begin here
The reason for this is that people should always be able to exit the conversation (closing words) or ask for help, no matter their stage. Thus, these checks come before anything else.
The SEND_LIST
check needs to be above NO_CONTACT
. The user is in this stage if they just got sent the list of available tools with the “view more” button. If they click that button, we handle that here. But if they send a different message instead, we need to be able to handle that like a new conversation. Hence setting the user stage to NO_CONTACT
so that the message gets handled by the appropriate block.
In NO_CONTACT
, some parts of it set the user stage, such as identifying that the user wants to return or check out, so that it can be handled by the appropriate code block. Other parts of it send a return message, if the message doesn’t require a stage change for the user.
Basically, the ordering of these is very intentional so that a user can flow through if-blocks as appropriate, and checks come in the proper order.
Making additions to Loan Wrangler's conversational abilities mostly happens in the above determine_response_for_user
function. For example, let’s say we want to make a message path where if a user sends the word “olin” then Loan Wranger will only respond to further messages with the word “olin”. It stops when a user says one of the default stop commands, of course.
First, we would add a check to the NO_CONTACT
if-block that changes the user stage if we see the start word “olin”:
# checking out
elif any(word in message for word in self.checkout_words):
user['stage'] = self.WANT_CHECKOUT
print(user['stage'])
# NEW CODE HERE:
elif “olin” in message:
user[‘stage’] = self.OLIN
# END NEW CODE
else:
# send greeting and ask what tool
response = "😄 Hi there! I'm Loan Wrangler, what can I help you with?"
return user, response, None
This also requires us to define the new user stage in the constructor:
self.CONFIRM_TOOL_RETURN = 8
self.AVAILABILITY_QUESTION = 9
self.SEND_LIST = 10
self.OLIN = 11 # NEW CODE
Finally, we make an if-statement in the rest of the function call, for handling messages when the user is in the new stage:
if user['stage'] == self.OLIN:
return user, "olin", None
Now Loan Wrangler would return the response "olin" to anything it was sent (except a stop word to end the conversation).
If we wanted to make a better addition, this function might actually do some handling of the message received. The structure of the return block is very specific; it returns a 3-part tuple:
- The user variable, which was passed into this function. It may have been updated over the course of running (such as updating its stage) so it needs to be passed back so it can be saved.
- The response string that should be send to the user in the chat window.
- The quickreply options. This should either be
None
if no quickreply options are available, or a list of strings that represents the list of quickreply options.
- Expand tests. Right now, testing coverage is low, and the most important functionalities to test (the
if
structure flow of theconversationHandler
) are not being tested at all. A good test suite might be some kind of matrix of responses and user stages, and testing that a given message begets a given response, depending on the user’s stage. Part of this would also include getting our Travis CI up again, and actually using it as an evaluator of our app. - Refactor. It’s not a great structure to rely so heavily on placement of code, the way that having some
if
statements before others is crucial or it breaks the code. This could probably be done better. - Integrate with TIND. Right now we’re pulling a tools list from TIND to populate our database, but that just parses XML, and only gets run when we execute it manually. All the transactions occur solely in our database. We should integrate with TIND’s self-check API so that the transactions are part of the library system.
- Security. There are no checks as to whether a user is a member of the Olin community. This should happen, through TIND users or another method.
If you are an Olin user working on the project for Olin, you can contact Anne LoVerso or Mimi Kome to get the app secrets, which are required for changing the Heroku deployment.
If you are outside Olin looking to adapt the project, you will need to create your own Facebook app and secrets. Creating a Facebook app is well documented
The deployment workflow uses Heroku. The Heroku account is dependent on our Mongo database. To replace this or use your own, initialize a new database via python3 create_tools_from_tind.py
.
The app needs access to the following environment variables:
validationToken
is a Facebook variable that confirms for Facebook that you own the app that you say you own. You can set this to anythingserverURL
is the link to your hosted webapp; for us, herokupageAccessToken
is generated by Facebook for your pagemongo_uri
is a MongoDB connection string URI.tind_access_token
will be needed to make Tind API requests. This is not currently used.
Loan Wrangler also relies on our Facebook page and its Messenger integration, which can be managed from the Facebook developers dashboard. Both the Library Facebook account Lib Guru and Anne are admins of this page and can make changes. The Facebook page is not yet approved for public messaging, which means that in order for the bot to respond to a given user, they need to be listed as a Developer, Tester, or Administrator role on the Facebook developer app site. When the app is public, this won’t be necessary. To make it public just submit an application for review on the Developer dashboard after making sure you’ve followed Facebook’s checkboxes, such as uploading a photo.
Code written by Anne LoVerso and Mimi Kome. Help from Oliver Steele, Jeff Goldenson, and Emily Ferrier. Thanks to the rest of the Hacking the Library class.
MIT License