May 2007 Archives

Asterisk Queues REDUX

| No TrackBacks

I love tinkering with stuff.

But sometimes it takes me too long to get something to work like it want it to.

We bit off a little more than we could chew with this current Asterisk dialplan. I've solved most of these problems as best I know how.

Here are the requirements (as far as I understand them):




  • Calls from queues should be distributed as fairly as possible

  • Calls from queues should cascade out to as many avalible agents as possible

  • Agents should be able to log into their assigned queues from any phone

  • Agents should be able to log into, and manage calls from multiple queues

  • Agent phones should only receive one call at a time from any of the queues they have logged into

  • Agent phones should be reachable via direct dialing, even when handling a call from a queue




Let's look at each of these items one at a time:

Calls from queues should be distributed as fairly as possible

The goal here is to give every agent at least a chance to answer a call if the agent before him/her doesn't. You might think that the answer is to do round-robin queueing, however that is incorrect, as the first agent will always get the first call after a lull of calling. We chose to use rrmemory queueing which is round robin with memory. Each time a call starts going around round robin, the system remembers where it last sent a call, and picks up from there the next time around.

Calls from queues should cascade out to as many avalible agents as possible

Originally in Asterisk the queues wouldn't send a call to another available agent until the first call had been picked up. To make a queue act more like a "queue should work" enable the option autofill=yes.

Here is a sample of my queues.conf file:

[general] persistentmembers=yes ; store agent login in astdb autofill=yes ; automaticlly dial as many agents as there are phone calls memberdelay=0 ; how long to wait until the agent is connected to the caller announce-frequency=90 ; how often do we announce "you are # in line" periodic-announce-frequency=30 ; how often do we announce "your call is really important to us" announce-holdtime=yes ; announce hold time to caller announce-round-seconds=10 ; do we round seconds? joinempty=yes ; allow people into a queue when there are no agents? reportholdtime=no ; tell agent how long the caller was holding ringinuse=no ; do we ring a extension which is 'in use'? retry=0 ; how long to wait until we re-ring the queue with the call (after a deny or busy etc?) timeout=20 ; how long to ring the agent before trying someone else wrapuptime=5 ; how long to wait after the call is completed (allows agent to clean up ticket, etc)

[pineapple]
strategy=rrmemory

[1]
strategy=rrmemory
reportholdtime=yes

[2]
strategy=rrmemory
reportholdtime=yes

[3]
strategy=rrmemory
timeout=20
retry=15
joinempty=no

Agents should be able to log into their assigned queues from any phone

To accomplish the truly mobile workforce we want, our agents need to be able to accept calls from various queues wherever they may be. We've solved this problem by using strictly dynamic queue agents. This means we don't pre-define agents in agents.conf. An example of the function to log agents in and out of a queue will be provided below.

Agents should be able to log into, and manage calls from multiple queues

We have need for persons sitting at the phones to be able to handle incoming calls from multiple queues. They need to be able to selectively log into and out of multiple queues easily.

Here is the sample of the queue login logout function

[macro-queueloginout] ; first argument is the queue to be added to, second(not yet implemented) is penality exten => s,1,Answer() ; Pick up the phone exten => s,n,Set(MYNUMB=${CUT(CHANNEL,-,1)}) ; clean up the channel name (remove unique id) exten => s,n,Set(MYNUMBCLEAN=${CUT(MYNUMB,/,2)}) ; clean up the channel name (remove technology) exten => s,n,Verbose(99|mynumb is ${MYNUMB} mynumbclean is ${MYNUMBCLEAN}) ; debugging output to see if VARS got set correctly exten => s,n,Set(OUR_QM_LIST=${QUEUE_MEMBER_LIST(${ARG1})}) ; assign member list to variable so it doesn't change exten => s,n,Set(CHANNEL_TO_MATCH=${EVAL(Local/${MYNUMBCLEAN}@queueagents/n)}) ; set what we're matching against exten => s,n,Set(X=1) ; initialize counter

; begin while loop
exten => s,n,While($[${EXISTS(${CUT(OUR_QM_LIST,\,,${X})})}]) ; while we still have a value (not-null), loop
exten => s,n,Set(OUR_ITTR=${CUT(OUR_QM_LIST,\,,${X})}) ; set our itterator to check
exten => s,n,Noop(prematch) ; debugging can probally be removed
exten => s,n,Set(MATCHED=${IF($["${OUR_ITTR}" = "${CHANNEL_TO_MATCH}"]?1:0)}) ; match against the channel
exten => s,n,Noop(postmatch) ; debugging can probally be removed
exten => s,n,Exec(${IF($[${MATCHED}]?ExitWhile():NoOp())}) ; exit while loop on match
exten => s,n,Set(X=$[${X} + 1]) ; increase the iterator
exten => s,n,EndWhile()
; end while loop
exten => s,n,GotoIf($[${MATCHED}]?100,1:400,1) ; branch on match, 100 means yes 400 means no

exten => 100,1,noop(yup) ; agent is already logged in, log em out
; TODO add confirmation "do you really want to logout?"
exten => 100,n,RemoveQueueMember(${ARG1},Local/${MYNUMBCLEAN}@queueagents/n) ; remove Local/*@queueagents instead of default
exten => 100,n,playback(agent-loggedoff) ; tell them the agaent logged off
exten => 100,n,Hangup

exten => 400,1,noop(nope) ; agent is not logged in, log em in
exten => 400,n,AddQueueMember(${ARG1},Local/${MYNUMBCLEAN}@queueagents/n,0) ; add Local/*@queueagents instead of default
; catch if queuemember breaks
exten => 400,n,Execif($[ ${AQMSTATUS} = ADDED ],Playback,agent-loginok) ; let the agent know they have sucessfully logged in
exten => 400,n,Execif($[ ${AQMSTATUS} = NOSUCHQUEUE ],Playback,try-again) ; if queue doesn't exist tell em to try again.
exten => 400,n,Hangup

Agent phones should only receive one call at a time from any queue

This requirement combined with the next one, became the biggest hurdle to overcome. If we restricted the number of calls via limits in sip.conf then the requirement below was not met, and if we didn't use limits in queues.conf then we had to disable any and all call-waiting on the phones(completely un-wanted side effect).

Agent phones should be reachable via direct dialing, even when handling a call from a queue

We ended up using the following tactic to satisify these two requirements. First, we didn't just add the channel that dialed the login macro, we added a related macro in the queueagents context. This is done by the following line in the macro posted above:

exten => 400,n,AddQueueMember(${ARG1},Local/${MYNUMBCLEAN}@queueagents/n,0) ; add Local/*@queueagents instead of default
We used the following logic in the queueagents context, which keeps track of which members are currently being called, and returns busy() if needed.
[queueagents] exten => _XXXX,1,noop(${EXTEN}) ; queue agent context, extension is agent to call exten => _XXXX,n,Set(GROUP(${EXTEN}@queueagents)=1) ; set the group to @queueagents exten => _XXXX,n,Set(MYCOUNT=${GROUP_COUNT(${EXTEN}@queueagents)}) ; retrieve the number of active calls exten => _XXXX,n,GotoIf($[${MYCOUNT} > 0]?busy) ; if the number of calls is greater than zero, goto the busy priority exten => _XXXX,n(avail),NoOp(avail) ; Availble priority, can be used to override call limits eg( goto queueagents,${EXTEN},avail ) exten => _XXXX,n,Set(OUTBOUND_GROUP=${EXTEN}@queueagents) ; Setup the magic OUTBOUND_GROUP varible, (increments the count next time someone attempts to call exten => _XXXX,n,Dial(SIP/${EXTEN}) ; if we got to here, then it's clear to call, dial away. TODO: Make the technoligy dynamic to allow IAX agents etc exten => _XXXX,n,Goto(done) ; skip over the busy priority exten => _XXXX,n(busy),NoOp(busy) ; busy priority exten => _XXXX,n,Busy() ; send busy to the queue exten => _XXXX,n(done),NoOp(done) ; done priority exten => _XXXX,n,Hangup() ; all done

One last thing we did was to override the caller ID so the agent could answer the phone according to the type of call. We did this just before calling the Queue() application in the dialplan.

exten => _54XX,1,Answer exten => _54XX,1,Wait(2) exten => _54XX,n,Playback(you-entered) exten => _54XX,n,SayAlpha(q) exten => _54XX,n,SayDigits(${EXTEN:3:1}) exten => _54XX,n,Set(CALLERID(name)=Queue -- ${EXTEN:3:1}) exten => _54XX,n,Queue(${EXTEN:3:1}) exten => _54XX,n,Hangup

Thanks and acknolegements

Getting this thing working wouldn't have been possible without Jared Smith's help, both direct help via IRC or indirect help via his book. If you're using Asterisk and don't have this book, you are working too hard. I would also like to thank those from the Utah Asterisk User's Group who chimed in the IRC channel. Now go buy Jared's book to help me feel less guilty about bothering him every day for the past three weeks.

Asterisk Queues

| No TrackBacks

I've been busy lately working on a rather big Asterisk installation.

One of the bigger problems we've wanted to solve was our queue management.

Here's the beginnings of a queue log in/out macro I've been fiddling with.

oh, and a super-big thanks goes to Jared Smith, for the basis of what I've done here.

; macros to do login and out of queues [macro-queueloginout] ; first argument is the queue to be added to, second(not yet implemented) is penality exten => s,1,Answer exten => s,n,Set(MYNUMB=${CUT(CHANNEL,-,1)}) exten => s,n,Set(MYNUMBCLEAN=${CUT(MYNUMB,/,2)}) exten => s,n,Set(OUR_QM_LIST=${QUEUE_MEMBER_LIST(${ARG1})}) ; assign member list to variable so it doesn't change exten => s,n,Set(CHANNEL_TO_MATCH=${CUT(CHANNEL,-,1)}) ; get rid of the unique identifier on the end exten => s,n,Set(X=1) ; initialize counter

exten => s,n,While($[${EXISTS(${CUT(OUR_QM_LIST,\,,${X})})}]) ; while we still have a value (not-null), loop
exten => s,n,Set(MATCHED=${IF($["${CUT(OUR_QM_LIST,\,,${X})"} = "${CHANNEL_TO_MATCH}"]?1:0)}) ; match against the channel
exten => s,n,Exec(${IF($[${MATCHED}]?ExitWhile():NoOp())}) ; exit while on match
exten => s,n,Set(X=$[${X} + 1]) ; increase the iterator
exten => s,n,EndWhile() ; End of the loop
exten => s,n,GotoIf($[${MATCHED}]?100,1:400,1) ;branch on match, 100 means yes 200 means no

exten => 100,1,noop(yup) ; agent is already logged in, log em out
exten => 100,n,RemoveQueueMember(${ARG1},Local/${MYNUMBCLEAN}@queueagents/n)
exten => 100,n,playback(agent-loggedoff)
exten => 100,n,Hangup

exten => 400,1,noop(nope) ; agent is not logged in, log em in
exten => 400,n,AddQueueMember(${ARG1},Local/${MYNUMBCLEAN}@queueagents/n,0)
; catch if queuemember breaks
exten => 400,n,Execif($[ ${AQMSTATUS} = ADDED ],Playback,agent-loginok)
exten => 400,n,Execif($[ ${AQMSTATUS} = NOSUCHQUEUE ],Playback,try-again)
exten => 400,n,Hangup

It is still a work in progress, I'm hoping to get a penality overload built into the macro in the near future.

Oh, yeah! I almost forgot to show you how to use the macro in your dialplan:

exten => _77XX,1,Macro(queueloginout,${EXTEN})

This extension, anything in the 7700 range, will log the currently calling phone into the queue specified, numbered by the extension dialed.
E.g. dial 7703 will log you into or out of queue 7703.

UPDATE 2007-05-10

I was fiddling with this, and realzed that only one agent could log in at a time.

Thanks again to Jared for discovering that adding four little quote marks makes it work correctly.

post above edited to show the new quotes.