New Adhearsion Feature: Dialplan Menus 5
Those who watch the Adhearsion trunk commits probably noticed a new feature I snuck in recently: the menu() dialplan command. Now that I’ve had a chance to actually use the feature myself, it’s time I cover it in more detail here.
For the record, I’m totally excited about it. I’ve wished for something like this for over a year and only recently did I discover a way to get around the one technical limitation to implement it.
The problem
The menu() command solves the problem of building enormous input-fetching state machines in Ruby without first-class message passing facilities or an external DSL. After completely scrapping the feature several times and starting over, I eventually settled on my design of a state machine using a second-class message passing pattern. I’ll be writing about this pattern soon here.
Meet menu()
For now, here’s an example of menu():
from_pstn {
menu 'welcome', 'for-spanish-press-8', 'main-ivr',
:timeout => 8.seconds, :tries => 3 do |link|
link.shipment_status 1
link.ordering 2
link.representative 4
link.spanish 8
link.employee 900..999
link.on_invalid { play 'invalid' }
link.on_premature_timeout do |str|
play 'sorry'
end
link.on_failure do
play 'goodbye'
hangup
end
end
}
shipment_status {
# Fetch a tracking number and pass it to a web service.
}
ordering {
# Enter another menu that lets them enter credit card
# information and place their order over the phone.
}
representative {
# Place the caller into a queue
}
spanish {
# Special options for the spanish menu.
}
employee {
dial "SIP/#{extension}" # Overly simplistic
}If you haven’t, take a minute to read the dialplan above before reading on.
The main detail to note is the declarations within the menu() command’s block. Each line seems to refer to a link object executing a seemingly arbitrary method with an argument that’s either a number or a Range of numbers. The link object collects these arbitrary method invocations and assembles a set of rules. The seemingly arbitrary method name is the name of the context to which the menu should jump in case its argument (the pattern) is found to be a match.
With these context names and patterns defined, the menu() command plays in sequence the sound files you supply as arguments, stopping abruptly if the user enters a digit. If no digits were pressed when the files finish playing, it waits :timeout seconds. If no digits are pressed after the timeout, it executes the on_premature_timeout hook you define (if any) and then tries again a maximum of :tries times. If digits are pressed that result in no possible match, it executes the on_invalid hook. When/if all tries are exhausted with no positive match, it executes the on_failure hook after the other hook (e.g. on_invalid, then on_failure).
When the menu() state machine runs through the defined rules, it must distinguish between exact and potential matches. It’s important to understand the differences between these and how they affect the overall outcome:
| exact matches | potential matches | result |
|---|---|---|
| 0 | 0 | Fail and start over |
| 1 | 0 | Match found! |
| 0 | 1 | Get another digit |
| 0 | >1 | Get another digit |
| >1 | 0 | Go with the first exact match |
| 1 | >0 | Get another digit. If timeout, use exact match |
| >1 | >0 | Get another digit. If timeout, use first exact match |
Database integration
To do database integration, I recommend programatically executing methods on the link object within the block. For example:
menu do |link|
for employee in Employee.find(:all)
link.internal employee.extension
end
endor this more efficient and Rubyish way
menu do |link|
link.internal *Employee.find(:all).map(&:extension)
endIf this second example seems like too much Ruby magic, let me explain — Employee.find(:all) effectively does a “SELECT * FROM employees” on the database with ActiveRecord, returning (what you’d think is) an Array. The map(&:extension) is fanciness that means “replace every instance in this Array with the result of calling extension on that object”. Now we have an Array of every extension in the database. The splat operator (*) before the argument changes the argument from being one argument (an Array) into a sequence of n arguments, where n is the number of items in the Array it’s “splatting”. Lastly, these arguments are passed to the internal method, the name of a context which will handle dialing this user if one of the supplied patterns matches.
Handling a successful pattern match
Which brings me to another important note. Let’s say that the user’s input successfully matched one of the patterns returned by that Employe.find... magic. When it jumps to the internal context, that context can access the variable entered through the extension variable. This was a tricky design decision that I think, overall, works great. It makes the menu() command feel much more first-class in the Adhearsion dialplan grammar and decouples the receiving context from the menu that caused the jump. After all, the context doesn’t necessary need to be the endpoint from a menu; it can be its own entry point, making menu() effectively a pipeline of re-creating the call.
I encourage you to give this command a try and let me know what you think! Feel free to post on the mailing list or here on my blog.
Enjoy, folks!
Great post! It’s another knowledge I’ve learned. The command seems not hard to follow and not complicated. I think I’ll enjoy using it.
Thanks for the long decription. As far as I can figure out, you will be linking to other contexts. How to use this in components? Another subject on which I’m sure I’m missing out on quite some great tricks.
Harry, I’ll be visiting the components system soon. The “helpers” approach in 0.8.0 was overly simplistic and I feel that the approach for components now is overly complex. It needs some refactoring before I’m content and can blog about it.
By the way guys, I fixed a HUGE typo in the example above (kudos if you noticed it like Tony Arcieri). Before, it said:
and it’s been corrected to be
Silly mistake. :)
Sweet! I like the feature Jay. Just used it in a pet project I’m working on. Makes putting together a menu snap. Just have one question for yourself or anyone reading.
Is there a way to easily reference the model that was just retrieved in the block from the context that is called. For example I have.
menu ‘beep’, :timeout => 8.seconds, :tries => 3 do |link| for audio in Audio.find(:all) link.internal audio.key_selection end end
which will call my internal context, but inside the internal context I want to retrieve additional information from the Audio model that was fetched. Any way to pass this Audio model right into my internal context? If not, maybe an enhancement to look at in the future.
internal { # I have access to the ‘extension’ variable, and can look up my Audio again, but would like to save another SQL query if possible. }
Adhearsion continues to amaze me at the work that it does in such a small amount of code.
Yeah…I’m trying to learn this stuff with my roommate and posts like this make all the difference. thx