rapidsms-decisiontree-app

This application is a generic implementation of a decision tree, which is completely database-configurable. Users are asked questions and respond via SMS messages using the RapidSMS framework built on top of Django.

The original code for this application was written by Dimagi and is currently packaged and maintained by Caktus Consulting Group, LLC.

Requirements

rapidsms-decisiontree-app is tested on RapidSMS 0.19, Django 1.7, and Python 2.7. There is optional support for django-celery.

Features

  • Support for sessions (i.e. 100 different users can all go through a session at the same time)
  • Branching logic for the series of questions
  • Tree visualization
  • Errors for unrecognized messages (e.g. ‘i don’t recognize that kind of fruit’) and multiple retries before exiting the session

Installation

The latest stable release of rapidsms-decisiontree-app can be installed from the Python Package Index (PyPi) with pip:

pip install rapidsms-decisiontree-app

Once installed you should include 'decisiontree' in your INSTALLED_APPS setting.

INSTALLED_APPS = (
    ...
    'decisiontree',
    ...
)

You’ll need to create the necessary database tables:

python manage.py migrate decisiontree

At this point data can only be viewed/changed in the Django admin. If you want to enable this on the front-end you can include the decisiontree.urls in your root url patterns.

urlpatterns = [
    ...
    url(r'^surveys/', include('decisiontree.urls')),
    ...
]

See the full documentation for additional configuration options.

Running the Tests

Test requirements are listed in requirements/tests.txt file in the project source. These requirements are in addition to RapidSMS and its dependencies.

After you have installed 'decisiontree' in your project, you can use the Django test runner to run tests against your installation:

python manage.py test decisiontree decisiontree.multitenancy

Minimal test settings are included in decisiontree.tests.settings; to use these settings, include the flag --settings=decisiontree.tests.settings.

To easily run tests against different environments that rapidsms-decisiontree-app supports, download the source and navigate to the rapidsms-decisiontree-app directory. From there, you can use tox to run tests against a specific environment:

tox -e python2.7-django1.7.X

Or omit the -e argument to run tests against all environments that rapidsms-decisiontree-app supports.

To see the test coverage you can run:

coverage run python manage.py test decisiontree decisiontree.multitenacy
coverage report -m

A common .coveragerc file is include in the repo.

Contents:

Decision Tree Overview

The tree app allows you to define decision trees that can perform a question-and-answer type interaction with a user. A tree consists of one or more states each of which is associated with a question and zero or more answers to that question that can transition to other states. As questions are answered the user traverses the tree based on the answers until he or she reaches a state that has no more transitions. At this point the user has completed the session. The tree saves every question/answer pairing in a single table, and provides functionality for applications to initiate a callback when trees are initiated and completed so that application developers can write their own processing of the tree data. A visualization of a tree is below.

_images/demo_tree.png

Making a Tree

Currently trees can only be made through the admin interface.

Trees have a trigger, which is is the incoming message that will initiate a tree. They also have a root state which is the first state the tree will be in. The question linked to the root state will be the one that is sent when the tree is initiated. The remaining logic of the tree is encapsulated by the Transition objects, which define how answers to questions move from one state to the next (more on this below).

A tree also has optional completion text, which is the message that will be sent to the user when they reach a node in the tree with no possible transitions.

Questions/Answers/TreeStates/Transitions

The behavior of a tree is fully encapsulated a set of states and transitions that define how one moves through the tree.

A Question is just some text to be sent to the user, and an optional error message if the question is not answered properly.

A TreeState is a location in a tree. A TreeState is associated with a Question (that will be asked when the user reaches that state in the Tree) and a set of Answers (Transitions) that allow traversal to other TreeStates.

A Transition is a way to move from one TreeState to another. A Transition has a beginning state, an Answer, and an optional ending state. If a transition has no ending state, the answer will result in the completion of the tree.

An Answer is a way to answer a question, and defines how one moves across a Transition

There are three possible types of answers:

1. The simplest is an exact answer. Messages will only match this answer if the text is exactly the same as the answer specified.

2. The second is a regular expression. In this case the system will run a regular expression over the message and match the answer if the regular expression matches.

3. The final type is custom logic. In this case the answer should be a special keyword that the application developer defines. The application developer can then register a function tied to this keyword with the tree app and the tree app will call that function to see if the answer should match. The function should return any value that maps to True if the answer is valid, otherwise any value that maps to False.

Registering a custom answer handler

The following code shows a function, and how to register that function with the Tree app as a custom answer handler for the word “demo”.

# inside myapp.App

def validate_password(self, msg):
    """
    This function validates a password. This exact functionality could
    have been provided with a normal answer type, but you can put
    whatever logic you want here as long as you return True/False
    for matching/non-matching answers.
    """
    return msg.text == "spomc"

def start(self):
    """Start is called by the router to bootstrap our app"""
    # get the app from the rapidsms router
    self.tree_app = self.router.get_app("tree")
    # register our validate password function with the "demo" keyword
    self.tree_app.register_custom_transition("demo", self.validate_password)

Notifications

For emailed notifications associated with tags to work, INSTALLED_APPS must contain ‘rapidsms.contrib.scheduler’ and the setting DECISIONTREE_NOTIFICATIONS must be True. The default is False.

Timeouts

decisiontree can notice when it’s been waiting for a response for too long and send a reminder, repeating the question. This requires running celery and celerybeat, and the additional configuration in settings.py.example.

On timeout, decisiontree will act as if it has received an invalid response. This results in sending a reminder and repeating the question, or, if the allowed retries are exhausted, giving up.

Simple Tree Example

Below is the setup for an example survey which asks three basic questions to a user. There are no branches and all responses allow for free text. The questions are based on the bridgekeeper scene from “Monty Python and the Holy Grail”.

Creating Questions

We will create the three questions via the admin interface with the following data:

# Questions
pk: 1
text: What is your name?

pk: 2
text: What is your quest?

pk: 3
text: What is your favorite color?

Note

The pks are listed for future reference but these would be auto generated by the database.

Creating Answers

Next we will create an allowable answer in the admin. These questions don’t require exact matches so we will just accept any text:

# Answers
pk: 1
name: Free Text
type: Regular Expression
answer: .*

Note

This regular expression will match anything. You may need to make this express less allowing depending on your needs.

Associating Questions and Answers

Questions and answers are associated via tree states and transitions. Since we don’t have any branches the tree states and questions will be tied in a one to one fashion:

# Tree States
pk: 1
question: 1

pk: 2
question: 2

pk: 3
question: 3

Within a tree state you can also optionally specify the number of allowable retries but these have been excluded for simplicity.

Transitions determine how users are moved through the tree based on their answers. Here we will allow any text for each question and simply move the user on to the next question:

# Transitions
pk: 1
current state: 1
answer: 1
next state: 2

pk: 2
current state: 2
answer: 1
next state: 3

pk: 3
current state: 3
answer: 1
next state: null

Transitions can also be tagged and notifications can be sent when those tags are triggered. For instance you may create a new answer which is for the text ‘blue’ and have a new transition which handles the case when the user had the favorite color blue. Again this has been excluded for simplicity.

The Survey Tree

At this point the tree structure is in place to ask the series of questions but we need a way to start the survey. Again in the admin we would create a Tree with a trigger keyword which asks the user the first question:

# Tree
pk: 1
trigger: #test
root state: 1
completion text: Go on. Off you go.

Now when an incoming SMS matches the trigger text #test we will respond with the question from state one “What is your name?”. They will proceed on with each question and when finished we will respond “Go on. Off you go.” This completion text is optional.

At this point we have a simple yet functioning linear survey tree. An example SMS workflow is given below:

555-555-1234 >>> #test
555-555-1234 <<< What is your name? # This is state 1, question 1
555-555-1234 >>> My name is Sir Lancelot of Camelot.
555-555-1234 <<< What is your quest? # This is state 2, question 2
555-555-1234 >>> To seek the Holy Grail.
555-555-1234 <<< What is your favorite color? # This is state 3, question 3
555-555-1234 >>> Blue.
555-555-1234 <<< Go on. Off you go. # End of questions

Continuing reading to see how we can add branches to this series of questions.

Advanced Tree Example

Here we will expand on the previous tree example to contain branches based on varying answers. We will continue to work from the Monty Python questions.

New Questions

First will add new questions for the addition branches:

# Questions
pk: 4
text: What is the capital of Assyria?

pk: 5
text: What is the air-speed velocity of an unladen swallow?

New Answers

In the same logic of the movie we will ask the questions based on the name of the knight:

# Answers
pk: 2
name: Sir Robin
type: Regular Expression
answer: .*Robin.*

pk: 3
name: King Arthur
type: Regular Expression
answer: .*Arthur.*

New States and Transitions

We need additional states to handle these trees. Since the final question is the only thing different we still need multiple states which point to the same second question:

# Tree States
pk: 4
question: 4

pk: 5
question: 5

pk: 6
question: 2

pk: 7
question: 2

Now we need the transitions between the questions based on the first answer. First we will do Sir Robin:

# Transitions
pk: 3
current state: 1
answer: 2
next state: 6

pk: 4
current state: 6
answer: 1
next state: 4

pk: 5
current state: 4
answer: 1
next state: null

Then King Arthur:

# Transitions
pk: 6
current state: 1
answer: 3
next state: 7

pk: 7
current state: 7
answer: 1
next state: 5

pk: 8
current state: 5
answer: 1
next state: null

The tree itself does not need to be modified. An example SMS workflow is given below:

555-555-1234 >>> #test
555-555-1234 <<< What is your name? # This is state 1, question 1
555-555-1234 >>> Sir Robin
555-555-1234 <<< What is your quest? # This is state 6, question 2
555-555-1234 >>> To seek the Holy Grail.
555-555-1234 <<< What is the capital of Assyria? # This is state 4, question 4
555-555-1234 >>> I don't know that.
555-555-1234 <<< Go on. Off you go. # End of questions

So this isn’t perfect to movie but it should demonstrate the difference from the simple example.

Available Settings

rapidsms-decisiontree-app has a few settings available for configuring the behaviour.

DECISIONTREE_NOTIFICATIONS

Default: False

If enabled this will periodically send emails based on the response tags and the TagNotification configurations. This requires the rapidsms.contrib.scheduler app.

DECISIONTREE_SESSION_END_TRIGGER

Default: end

This configures a keyword which the users can use to end their question session. This functionality can be disabled by making this setting None.

DECISIONTREE_TIMEOUT

Default: 300

This is the time in seconds to wait between questions before the user is asked the question again or the question session is abandoned. Using this setting requires the threadless-router and django-celery. You must enable this task in your CELERYBEAT_SCHEDULE in your project settings

from celery.schedules import crontab

CELERYBEAT_SCHEDULE = {
    # Other periodic tasks included here
    "decisiontree-tick": {
        "task": "decisiontree.tasks.PeriodicTask",
        # How often to check sessions for timeout
        "schedule": crontab(),  # every minute
    },
}

Release History

Below is the history of the rapidsms-decisiontree project. With each release we note new features, large bug fixes and any backwards incompatible changes.

v0.1.0 (TBD)

The initial PyPi release.

Indices and tables