Engineering

What is a phone tree?

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:

Simple IVR

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 if they are calling to file a complaint to point them to the complaints form. But only if they call from a mobile phone!

Oh and we want to play a message for international customers warning them about the phone charges.

Complex IVR

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.

Bender two things

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 can 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 Automaton

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 the purpose of this blog post we’re not going to go deep into theory. Here, we’re going to 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 that has 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.

State machine

Here S0 and S1 are states and 0 and 1 are inputs. Given a state and an input you can always tell what the next state of the machine would be.

So how does it help with our phone support problem?

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 certain state at any given point - it’s playing a message or 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 on 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 a 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.

IVR state machine

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 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 largely the same.

And just like that, business requirements have been defeated by computer science 🤓

October 04, 2021

Irina Bednova

CTO