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 the 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 implicitly 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. 029×1235551212.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 line 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:
- When 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:
- Voximal The VoiceXML interpreter for Asterisk (create voice portals with VoiceXML W3C standard language and integrate TTS and ASR in your interactions)
- Cally Square HTML5 Drag and Drop IVR for ASTERISK
- http://www.dobesoft.com Execute AGI scripts AGIntera ASTERISK IVR
- phpivr project by Grygorii Maistrenko
- Tower Technologies AstrIVR for an integrated IVR
- Asterisk IVR tutorial with real inbound/outbound dialing.
- Visual IVR Call Flow with web based designer from Anveo
- eCapture a pay as go online survey tool that integrates with a vicidial asterisk platform
- Visual Dialplan Builder is a Visual GUI tool for building powerful IVR for Asterisk based contact centers.
See Also
- Asterisk tips phrase recording menu
- Asterisk tips and tricks
- Dialplan Extension Matching
- The Dialplan – extensions.conf
- Commands: Asterisk cmd Playback | Record | Asterisk cmd DigitTimeout | Asterisk cmd ResponseTimeout | Wait
- phpivr AGI installation guide