If you have ever called a company and heard “Press 1 for new customers, press 2 for existing customers…” or something to that extent, then you have encountered an IVR - Interactive Voice Response. In its most basic form, an IVR tree will look something like this:
And when you try to code this up, it looks pretty straightforward
digit = request.form.["Digit"]
if digit == "1":
redirect_to_sales()
if digit == "2":
redirect_to_cs()
if digit == "3":
collect_voicemail()
More options - more problems
Things start to get hairy when business needs get more complicated. First, we want to add another layer for “Sales” - for corporate and individual customers.
Then for customer support, we want the caller to say their name before connecting to the agent for easier identification. For out-of-hours support, we want to ask customers to leave a voicemail so that agents can call back the next day.
We also want to send customers an SMS to point them to the complaints form if they are calling to file a complaint. But only if they call from a mobile phone!
Oh, and we want to play a message to international customers warning them about the phone charges.
And that’s just the start! You know the Head of Customer Engagement is up for a raise this year, so they’re full of ideas. WhatsApp messages? Voice bots? Nothing’s off the table.
Pretty soon, you're neck deep in the nested 'if' statements. And then, the company decides they need a web interface for this, so anyone could change the config without taking away engineering time.
That's where you know you need to come up with a better solution. Ideally, something that shows off how clever you are. Well, I've got just the thing for you.
Deterministic Finite Automation
DFA is one of the foundational concepts of the automata theory. A lot of stuff we know and love is built on top of the State Machine - including Turing machines and regular expressions.
However, for this blog post, we’re not going to go deep into theory. Here, we will be using DFA as an abstraction, a design pattern, in hopes that it will help us write better, more easily extendable code.
So what is a Deterministic Finite Automaton?DFA (or a Finite State Machine) is an automata theory concept describing a machine with a finite number of states and a finite set of transitions.
What makes it deterministic is that given a state and a transition, you can always deterministically tell what the next state is.
How to describe an arbitrary IVR tree as a DFA
We can represent the IVR tree as a state machine.
Our IVR system is in a particular state at any given point - playing a message, sending a text, or hanging up.
Every time something happens in the system - an inbound call, a customer pressing a digit on the dial pad, a lookup function telling us whether the caller is on a mobile or the landline - it’s an input.
All we need to do is to define what should happen in every state and which input leads to the next state.
Let’s start with the simple IVR we saw earlier - a customer calls, presses “1”, “2”, or “3”, and gets redirected to the correct department.
Now to the most important part - how do we represent this in code?
We will define a dictionary where keys are states and values are structures containing:
- what should happen in the state;
- transitions from this state to the next.
For the simple IVR, it will look like this:
states = {
"start": {
"transitions": {
"incoming_call": "greeting"
}
},
"greeting": {
"action": "play_greeting_message",
"transitions": {
"pressed_1": "call_sales",
"pressed_2": "call_cs",
"pressed_3": "call_complaints"
}
},
// These three states have no transitions - they are terminal states
"call_sales": {
"action": "dial_sales"
},
"call_cs": {
"action": "dial_cs"
},
"call_complaints": {
"action": "dial_complaints"
}
}
Now to determine what needs to happen at any given time, we just need to keep track of the previous step and the transition that happened after.
previous_state = request.form.get("previous_state", "")
transition = request.form.get("transition", ||)
state = None
# The very first state will not have a previous state
if not previous_state:
state = "start"
else:
# If a state does not have transitions it's a terminal state
if not states[previous_state].get("transitions", {}):
return
state = states[previous_state]["transitions"][transition]
action = states[state]["action"]
# Process the action here
As the IVR tree grows more complex, it’s a matter of adding steps, transitions and action processors - like sending an SMS or capturing a voicemail - to our config dictionary. The processing code stays essentially the same.
And just like that, business requirements have been defeated by computer science.