Interactive voice response menus
Implementing a simple 'push-1, push-2' menu structure
The key to creating this menu is to create an Extension (defined as 205 below) to record your menu prompts. This will put the sound file in
/tmp/asterisk-recording.gsm. You'll have to move that file each time its created to /var/lib/asterisk/sounds and rename it to something pertinent to your design so it can be called from the dial-plan. Notice the line under [mainmenu]
exten => s,5,Background(sai-welcome). The
sai-welcome is one of those .gsm sound files. The rest of the dial-plan just defines what happens when each option is pushed. If you want to be able to have regular users update the voice prompts, see
asterisk tips phrase recording menu.
extensions.conf
[mainmenu]
exten => s,1,Answer
exten => s,2,SetMusicOnHold(default)
exten => s,3,DigitTimeout,5
exten => s,4,ResponseTimeout,10
;SAI menu - 1 for tech support, 2 for voicemail, 3 for echo test
exten => s,5,Background(sai-welcome)
exten => s,6,Background(sai-choose)
; Tech Support
exten => 1,1,AGI(dima-test.agi)
exten => 1,2,SetGlobalVar(ACCOUNTCODE=${callerid})
exten => 1,3,SetVar(testcallerid=${callerid})
exten => 1,4,Background(sai-reptech-welcome)
exten => 1,5,Queue(rep-tech)
; Leave Voicemail
exten => 2,1,VoicemailMain()
exten => 2,2,Hangup
; Echo Test
exten => 3,1,Playback(demo-echotest)
exten => 3,2,Echo
exten => 3,3,Playback(demo-echodone)
exten => 3,4,Goto(mainmenu,s,6)
; EAGI Test
exten => 4,1,Answer()
exten => 4,2,Wait(1)
exten => 4,3,AGI(sai-repid.agi)
exten => 4,4,Wait(1)
exten => 4,5,Hangup
; Play Music-on-Hold
exten => 5,1,MusicOnHold(default)
exten => 5,2,Goto(mainmenu,s,6)
; #=hangup
exten => #,1,Playback(sai-thanks)
exten => #,2,Hangup
exten => t,1,Goto(#,1) ; If they take too long, give up
exten => i,1,Playback(invalid) ; "That's not valid, try again"
[default]
include => mainmenu
include => local
include => longdistance
include => joe-iax
include => npi-iax
; Record voice file to /tmp directory
exten => 205,1,Wait(2) ; Call 205 to Record new Sound Files
exten => 205,2,Record(/tmp/asterisk-recording:gsm) ; Press # to stop recording
exten => 205,3,Wait(2)
exten => 205,4,Playback(/tmp/asterisk-recording) ; Listen to your voice
exten => 205,5,wait(2)
exten => 205,6,Hangup
Example menu with timeout and invalid option. Works with Asterisk 1.6
exten => s,1,Set(NUMINVALID=0)
exten => s,2,Set(NUMTIMEOUTS=0)
exten => s,3,Background(thank-you-for-calling)
exten => s,4,Set(TIMEOUT(digit)=5)
exten => s,5,Set(TIMEOUT(response)=10)
exten => s,6,WaitExten(5)
exten => t,1,Set(NUMTIMEOUTS=$[${NUMTIMEOUTS}+1]})
exten => t,2,Gotoif($["${NUMTIMEOUTS}" < "3"]?s,3)
exten => t,3,Background(vm-goodbye)
exten => t,4,Hangup()
exten => i,1,Set(NUMINVALID=$[${NUMINVALID}+1]})
exten => i,2,Gotoif($["${NUMINVALID}" < "4"]?:10)
exten => i,3,Background(invalid)
exten => i,4,Goto(s,3)
exten => i,10,Playback(vm-goodbye)
exten => i,11,Hangup()
Implementing a high-density without wearing out your keyboard
Now consider an information delivery
IVR, such as a bus schedule. The basic call flow goes something like:
Hear initial greeting
Hear root menu
Descend into to menu 1
Descend into to submenu 2
Play information message at option 4
Return to submenu 2
Descend into to submenu 7
Return to root menu
{and so on...}
The schedule that prompted creation of this code has 136 possible menu/message combinations.
[transit-ivr]
exten => s,1,Answer
exten => s,2,SetVar(ivrflag=transit) ; Unused at the moment
exten => s,3,Background(transit-ivr/grtg-3) ; Play the initial greeting (interruptable)
exten => s,4,Goto(transit-ivrmain,s,1) ; Start the menuing system after the message completes
exten => _x,1,Goto(transit-ivrmain,s,1) ; Start the menuing system if the user presses any key during the greeting
[transit-ivrmain]
exten => s,1,NoOp(s,1 - Has ${digitstack} in the digitstack)
exten => s,2,GotoIf($[${LEN(${digitstack})} = 0]?s-restart,1) ; If there is nothing in the stack at all then restart the stack
exten => s,3,System(/var/lib/asterisk/sounds/transit-ivr/filexists /var/lib/asterisk/sounds/transit-ivr/${digitstack}.gsm) ; test for the existance of this path as a recording
exten => s,4,Background(transit-ivr/${digitstack})
exten => s,5,NoOp() ; insert padding to offset s,9 so it in turn is offset cleanly enough for it's n+101 jump
exten => s,6,NoOp()
exten => s,7,NoOp()
exten => s,8,NoOp()
exten => s,9,System(/var/lib/asterisk/sounds/transit-ivr/filexists /var/lib/asterisk/sounds/transit-ivr/${digitstack}?.gsm) ; test for the existance of additional recordings underneath the current path
exten => s,104,NoOp(we should warn the user they chose an invalid option...) ; Relative to s,3
exten => s,105,SetVar(digitstack=${digitstack:0:$[${LEN(${digitstack})} - 1]})
exten => s,106,Goto(s,1)
exten => s,110,NoOp(there aren't any menus below this one, so go back up one level) ; Relative to s,9
exten => s,111,SetVar(digitstack=${digitstack:0:$[${LEN(${digitstack})} - 1]})
exten => s,112,Goto(s,1)
exten => s-restart,1,SetVar(digitstack=0)
exten => s-restart,2,Goto(s,1)
; Capture keypresses
exten => _X,1,SetVar(digitstack=${digitstack}${EXTEN})
exten => _X,2,GotoIf($[${digitstack:$[${LEN(${digitstack})} - 1],1} = 8]?_X,10)
exten => _X,3,Goto(s,1)
exten => _X,10,SetVar(digitstack=${digitstack:0:$[${LEN(${digitstack})} - 2]}) ; Eight pops two digit off the stack... (including the 8 itself)
exten => _X,11,Goto(s,1)
'.../transit-ivr/filexists' is a bash script containing:
#!/bin/bash
# Test for the existance of files based on shell globbing patterns
for a in $1;
do
if [ -f $a ] ; then
# there are one or more files that match;
exit;
else
# there aren't any files that match;
exit 1;
fi;
done;
Finally, the folder '.../transit-ivr/' contains a collection of .gsm recordings named according to their 'dialing path', thus:
8192 Aug 27 00:53 .
20480 Aug 27 00:53 ..
54285 Aug 25 22:46 0.gsm This is the root menu
66429 Aug 25 22:47 01.gsm This is menu 1 off of the root
17556 Aug 25 22:46 02.gsm This is menu 2 off of the root
17292 Aug 25 22:44 023.gsm This is informational recording 3 off of menu 2 off of the root
115533 Aug 25 22:48 03.gsm This is menu 3 off the root
{and so on...}
When creating the recordings, any recording that doesn't have another recording 'below' it in 'dialing path' is implicity an informational recording (a leaf on the tree). To work backwards up the tree 8 (or any other key) has to be assigned to 'go back to the previous menu'. Other global keys such as 'exit to operator' and 'end call/hangup' can be assigned similarly using dialplan logic.
The next refinement to this model would be to add a filename suffix parser to be able to embed jumps, transfers and out-dials inside the tree by encoding them into the filenames (ie. 029x1235551212.gsm).
All of this should be painfully familiar if you've worked with Nortel Norstar NAM3 based StarTalk IVRs before.
Advanced IVR Menu
Here is a more advanced
IVR application that is assumed you know how it works. I found this example in a newsgroup but I don't have the url to reference. It was very helpful for me to see this type of setup different from the ones provided above.
This shows the flow between multiple IVR menus and how to control and display the information. I will show the basics of the IVR.
[deeper]
exten => s,1,Background(demo-nomatch)
;exten => s,1,AGI(festival-script.pl|"deeper menu")
exten => s,2,Goto,options|s|1
[options]
exten => s,1,Background(demo-instruct)
;exten => s,1,AGI(festival-script.pl|"options menu")
exten => 1,1,Goto,deeper|s|1
exten => 2,1,Goto,sales|s|1
[sales]
exten => s,1,Background(demo-thanks)
;exten => s,1,AGI(festival-script.pl|"sales menu")
exten => 0,1,Goto,from-sip|1000
exten => 1,1,Goto,Menu|s|1
[Menu]
exten => s,1,Background(demo-congrats)
;exten => s,1,AGI(festival-script.pl|"menu menu")
exten => 1,1,Goto,sales|s|1
exten => 2,1,Goto,options|s|1
exten => i,1,Goto,s
exten => t,1,Goto,s
;The specific method to enter the menu is up to you but I used my sip phone to test.
[from-sip]
exten => 100,1,Answer
exten => 100,2,Goto,Menu|s|1
If you have sip configured this should work out of the box. I am not sure if it requires that you have the sample installed to have the default sound files but this is what i have setup above. If you have festival configured on the box then you can use the commented out lise for the festival-script.pl which does not require the running festival server but rather just have the binaries in the path.
The important piece that I really didn't realize on this site is the Goto that allow you to go to a context which was a simple piece that I was missing"exten => 100,2,Goto,Menu|s|1" or "exten => 100,2,Goto(Menu,s,1)". So each menu is a context and I can move back and forth through each one for a more advanced IVR setup.
Here is a portion of that listed above that accepts the call and plays the message. And accepts a 1 or 2 as menu options to change the option. When the 1 is pressed then the IVR will go to the sales context and when the 2 is pressed then I will go to the options context.
[Menu]
exten => s,1,Background(demo-congrats)
exten => 1,1,Goto,sales|s|1
exten => 2,1,Goto,options|s|1
This may have been clear but from my reading I missed it and thought it should be added to the site.
IVR Menu with navigation history
Context
voicemenu-mymenu_level0 is a top level.
CUR_LEVEL - global variable store current position in menu, with navigation history, items separated by #
For example
voicemenu-mymenu_level0#voicemenu-mysubmenu1_level1#voicemenu-mysubmenu3_level2#
macro-mymenu_addlevel - macros to push level into history stack.
macro-mymenu_prevlevel - macros to pop level from history stack.
user can press
* at any level to go into previously visited menu level.
Advantages:
- wen you build menu, you don't care about reverse dependencies.
- don't use any external scripts
- possible to use same context in different branches with correct reverse navigation (look at voicemenu-mysubmenu3_level2 it available from voicemenu-mysubmenu1_level1 and voicemenu-mysubmenu2_level1)
;tested with Asterisk 1.6.2.5
[macro-mymenu_addlevel]
exten = s,1,NoOp(${MACRO_CONTEXT})
exten = s,2,Set(Count=${FIELDQTY(CUR_LEVEL,#)})
exten = s,n,Gotoif($[ "${CUT(CUR_LEVEL,#,$[${Count}])}" = "${MACRO_CONTEXT}" ] ]?s-exit) ;if last level in stack same as current context then exit.
exten = s,n,Set(PREV_LEVEL=${CUR_LEVEL})
exten = s,n,Set(__CUR_LEVEL=${IF($[ "${PREV_LEVEL}" != "" ]?${PREV_LEVEL}#:${PREV_LEVEL})}${MACRO_CONTEXT}); add MACRO_CONTEXT to stack
exten = s,n(s-exit),NoOp(Exit form macro-mymenu_addlevel)
[macro-mymenu_prevlevel]
exten = s,1,Set(Count=${FIELDQTY(CUR_LEVEL,#)})
exten = s,2,Set(TMP=${CUR_LEVEL})
exten = s,n,Set(TOGO=${CUT(TMP,#,$[${Count}-1])});Count alwayse >0
exten = s,n,Set(__CUR_LEVEL=${IF( $[${Count}>1]?${CUT(TMP,#,-$[${Count}-2])}:"")}); if count>1 set CUR_LEVEL, else clear it.
exten = s,n,NoOp(${CUR_LEVEL})
exten = s,n,Goto(${TOGO},s,1)
[voicemenu-mymenu_level0]
exten = s,1,Macro(mymenu_addlevel)
exten = s,n,Answer()
exten = s,n,Set(TIMEOUT(digit)=5)
exten = s,n,Set(TIMEOUT(response)=10)
exten = s,n,Background(greatings_1)
exten = s,n,WaitExten(5)
exten = 0,1,Dial(SIP/6051,300,r); fax
exten = 1,1,Goto(voicemenu-mysubmenu1_level1,s,1)
exten = 2,1,Goto(voicemenu-mysubmenu2_level1,s,1)
exten = i,1,Goto(s,1)
exten = t,1,Goto(voicemenu-mymenu-operator,s,1)
[voicemenu-mysubmenu1_level1]
exten = s,1,Macro(mymenu_addlevel)
exten = s,n,Background(payment_1)
exten = s,n,WaitExten(5)
exten = 0,1,Goto(voicemenu-mymenu-operator,s,1)
exten = 1,1,Goto(voicemenu-mysubmenu3_level2,s,1)
exten = *,1,Macro(mymenu_prevlevel)
exten = i,1,Goto(s,1)
exten = t,1,Goto(voicemenu-mymenu-operator,s,1)
[voicemenu-mysubmenu2_level1]
exten = s,1,Macro(mymenu_addlevel)
exten = s,n,Background(payment_2)
exten = s,n,WaitExten(5)
exten = 0,1,Goto(voicemenu-mymenu-operator2,s,1)
exten = 1,1,Goto(voicemenu-mysubmenu3_level2,s,1)
exten = *,1,Macro(mymenu_prevlevel)
exten = i,1,Goto(s,1)
exten = t,1,Goto(voicemenu-mymenu-operator2,s,1)
[voicemenu-mysubmenu3_level2]
exten = s,1,Macro(mymenu_addlevel)
exten = s,n,Background(howtodosomething)
exten = s,n,WaitExten(5)
exten = *,1,Macro(mymenu_prevlevel)
exten = i,1,Goto(s,1)
exten = t,1,Goto(voicemenu-mymenu-operator1,s,1)
Other way of doing this
The above are great ways to create simple IVR applications using Asterisk. But Asterisk is a PBX system, not a full featured IVR system. Capabilities like database connection, website connection, GUI design environment for IVR call flows are not the strong suite of PBX systems. Here are some of the options which may help with IVR development:
See Also