Automatic Call Distribution (ACD) Queues(part-2)[*ASTERISK*]
***Queue Members***
Queues aren’t very useful without someone to answer the calls that come into them, so we need a method for allowing agents to be logged into the queues to answer calls. There are various ways of going about this, so we’ll show you how to add members to the queue both manually (as an administrator, via either the CLI, or hardcoded in the queues.conf file) and dynamically (as the agent, through an extension defined in the dialplan). We’ll start with the Asterisk CLI method, which allows you to easily add members to the queue for testing and minimal dialplan changes. Next we’ll show how you can define members in the queues.conf file (which will add defined members whenever app_queue is reloaded). Finally, we’ll show you how to add dialplan logic that allows agents to log themselves into and out of the queues and to pause and unpause themselves in queues they are logged into.
Controlling Queue Members via the CLI
We can add queue members to any available queue through the Asterisk CLI command queue add. The format of the queue add
command is (all on one line):
*CLI>queue add member <channel> to <queue> [[[penalty <penalty>] as<membername>] state_interface <interface>]
The<channel> is the channel we want to add to the queue, such as SIP/0000FFFF0003, and the <queue> name will be something like support or sales—any queue name that exists in/etc/asterisk/queues.conf. For now we’ll ignore the <penalty> option. We can define the <membername> to provide details to the queue-logging engine.
The state_interface option is something that we need to take a closer look at. Because it is so important for all aspects of queues and their members in Asterisk.
Once you’ve set that up, come back here and continue on. Don’t worry, we’ll wait.
Now that you’ve added callcounter=yes to sip.conf (we’ll be using SIP channels throughout the rest of our examples), let’s see how to add members to our queues from the Asterisk CLI.
Adding a queue member to the supportqueue can be done with the queue add member command:
*CLI> queue add member SIP/0000FFFF0001 to support
Added interface ‘SIP/0000FFFF0001’ to queue ‘support’
A query of the queue will verify that our new member has been added:
*CLI> queue show support
support has 0 calls (max unlimited) in ‘rrmemory’ strategy
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
Members:
SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet
No Callers
To remove a queue member, you would use the queue remove member command:
*CLI>queue remove member SIP/0000FFFF0001 from support
Removed interface ‘SIP/0000FFFF0001’ from queue ‘support’
Of course, you can use the queue show command again to verify that your member has been removed from the queue.
We can also pause and unpause members in a queue from the Asterisk console, with the queue pause member and queue unpause member commands. They take a similar format to the previous commands we’ve been using:
*CLI> queue pause member SIP/0000FFFF0001 queue support reason DoingCallbacks
paused interface ‘SIP/0000FFFF0001’ in queue ‘support’ for reason ‘DoingCallBacks’
*CLI> queue show support
support has 0 calls (max unlimited) in ‘rrmemory’ strategy
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
Members:
SIP/0000FFFF0001 (dynamic) (paused) (Not in use) has taken no calls yet
No Callers
By adding a reason for pausing the queue member, such as lunchtime, you ensure that your queue logs will contain some additional information that may be useful. Here’s how to unpause the member:
*CLI>queue unpause member SIP/0000FFFF0001 queue support reason off-break
unpaused interface ‘SIP/0000FFFF0001’ in queue ‘support’ for reason ‘off-break’
*CLI>queue show support
support has 0 calls (max unlimited) in ‘rrmemory’ strategy
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
Members:
SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet
No Callers
In a production environment, the CLI would not normally be the best way to control the state of agents in a queue. Instead, there are dialplan applications that allow agents to inform the queue as to their availability.
Defining Queue Members in the queues.conf File
If you define a queue member in the queues.conf file, that member will always be logged into the queue, whenever it is reloaded. Typically, you would only use this method if you want to ensure a member is always logged in. Within each queue definition, you simply define the members thus:
[general]
; all the general settings we’ve discussed before
[sales](StandardQueue)
member => SIP/0000FFFF0005 ; or any other channel
[service](StandardQueue)
member => SIP/0000FFFF0006
In a typical queue (one in which you have a group of people responsible for answering calls), you will find that defining the members in the queues.conf file will not serve you well. Agents need to be able to log in and out (and not be automatically logged in whenever the queue is reloaded). We do not recommend defining members in the queues.conf file, unless they have some other purpose (such as a bank of devices that answer calls, where you want to use the queue to load-balance calls to the device pool,
or a ring group, where all phones ring for all calls all the time).
Controlling Queue Members with Dialplan Logic
In a call center staffed by live agents, it is most common to have the agents themselves log in and log out at the start and end of their shifts (or whenever they go for lunch, or to the bathroom, or are otherwise not available to the queue).
To enable this, we will make use of the following dialplan applications:
•AddQueueMember()
•RemoveQueueMember()
While logged into a queue, it may be that an agent needs to put herself into a state where she is temporarily unavailable to take calls. The following applications will allow this:
•PauseQueueMember()
•UnpauseQueueMember()
It may be easier to think of these applications in the following manner: the add and remove applications are used to log in and log out, and the pause/unpause pair are used for short periods of agent unavailability. The difference is simply that pause/unpause
set the member as unavailable/available without actually removing them from the queue. This is mostly useful for reporting purposes (if a member is paused, the queue supervisor can see that she is logged into the queue, but simply not available to take calls at that moment). If you’re not sure which one to use, we recommend that the agents use add/remove whenever they are not physically at their phone, and pause/unpause when they are at their desk, but temporarily not available
**Important Point- “The use of pause and unpause is a matter of preference. In some environments, these options may be used for all activities during the day that render an agent unavailable (such as during the lunch hour and when performing work that is not queue-related). In most call centers, however, if an agent is not beside his phone and ready to take a call at that moment, he should not be logged in at all, even if he is only going to be away from his desk for a few minutes (such as for a bathroom break). Some supervisors like to use the add/remove and pause/unpause settings as a sort of punch clock, so that they can track when their staff arrive for work and leave at the end of the day, and how long they spend at their desks and on breaks. We do not feel this is a sound practice, as the purpose of these applications is to inform the queue as to agent availability, not to enable tracking of employees’ activities. An important thing to note here relates to the joinempty setting in queues.conf, which was discussed earlier. If an agent is paused, he is considered logged into the queue. Let’s say it is near the end of the day, and one agent put himself into pause a few hours earlier to work on a project. All the other agents have logged out and gone home. A call comes in. The queue will note that an agent is logged into the queue, and will therefore queue the call, even though the reality is that there are no people actually staffing that queue at that time. This caller may end up holding in an unstaffed queue indefinitely. In short, agents who are not sitting at their desks and planning to be available to take calls in the next few minutes should log out. Pause/unpause should only be used for brief moments of unavailability (if at all). If you want to use your phone system as a punch clock, there are lots of great ways to do that using Asterisk, but the queue member applications are not the way we would recommend.”
Let’s build some simple dialplan logic that will allow our agents to indicate their avail‐ ability to the queue. We are going to use the
CUT() dialplan function to extract the name of our channel from our call to the system, so that the queue will know which channel
to log into the queue.
We have built this dialplan to show a simple process for logging into and out of a queue, and changing the paused status of a member in a queue. We are doing this only for a single queue that we previously defined in the queues.conf file. The status channel variables that the AddQueueMember(), RemoveQueueMember(), PauseQueueMember(), and UnpauseQueueMember() applications set might be used to Playback() announcements to the queue members after they’ve performed certain functions to let them know whether they have successfully logged in/out or paused/unpaused):
[QueueMemberFunctions]
exten => *54,1,Verbose(2,Logging In Queue Member)
same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})
same => n,AddQueueMember(support,${MemberChannel})
same => n,Verbose(1,${AQMSTATUS}) ; ADDED, MEMBERALREADY, NOSUCHQUEUE
same => n,Playback(agent-loginok)
same => n,Hangup()
exten => *56,1,Verbose(2,Logging Out Queue Member)
same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})
same => n,RemoveQueueMember(support,${MemberChannel})
same => n,Verbose(1,${RQMSTATUS}; REMOVED, NOTINQUEUE, NOSUCHQUEUE
same => n,Playback(agent-loggedoff)
same => n,Hangup()
exten => *72,1,Verbose(2,Pause Queue Member)
same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})
same => n,PauseQueueMember(support,${MemberChannel})
same => n,Verbose(1,${PQMSTATUS}); PAUSED, NOTFOUND
same => n,Playback(dictate/paused)
same => n,Hangup()
exten => *87,1,Verbose(2,Unpause Queue Member)
same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)})
same => n,UnpauseQueueMember(support,${MemberChannel})
same => n,Verbose(1,${UPQMSTATUS}); UNPAUSED, NOTFOUND
same => n,Playback(agent-loginok)
same => n,Hangup()
Automatically Logging Into and Out of Multiple Queues
It is quite common for an agent to be a member of more than one queue. Rather than having a separate extension for logging into each queue (or demanding information from the agents about which queues they want to log into), this code uses the Asterisk database (astdb) to store queue membership information for each agent, and then loops through each queue the agents are a member of, logging them into each one in turn. In order for this code to work, an entry similar to the following will need to be added to the AstDB via the Asterisk CLI. For example, the following would store the member 0000FFFF0001 as being in both the support and
sales queues:
*CLI> database put queue_agent 0000FFFF0001/available_queues support^sales
You will need to do this once for each agent, regardless of how many queues they are members of.
If you then query the Asterisk database, you should get a result similar to the following:
pbx*CLI>database show queue_agent
/queue_agent/0000FFFF0001/available_queues : support^sales
The following dialplan code is an example of how to allow this queue member to be automatically added to both the support and
sales queues. We’ve defined a subroutine that is used to set up three channel variables ( MemberChannel, MemberChanType, AvailableQueues). These channel variables are then used by the login (*54), logout(*56), pause (*72), and unpause (*87) extensions. Each of the extensions uses the subSetupAvailableQueues subroutine to set these channel variables and to verify that
the AstDB contains a list of one or more queues for the device the queue member is calling from:
[subSetupAvailableQueues]
;
; This subroutine is used by the various login/logout/pausing/unpausing routines
; in the [ACD] context. The purpose of the subroutine is to centralize the retrieval
; of information easier.
;
exten => start,1,Verbose(2,Checking for available queues)
; Get the current channel’s peer name (0000FFFF0001)
same => n,Set(MemberChannel=${CHANNEL(peername)})
; Get the current channel’s technology type (SIP, IAX, etc)
same => n,Set(MemberChanType=${CHANNEL(channeltype)})
; Get the list of queues available for this agent
same => n,Set(AvailableQueues=${DB(queue_agent/${MemberChannel}/
available_queues)})
; *** This should all be on a single line
; if there are no queues assigned to this agent we’ll handle it in the
; no_queues_available extension
same => n,GotoIf($[${ISNULL(${AvailableQueues})}]?no_queues_available,1)
same => n,Return()
exten => no_queues_available,1,Verbose(2,No queues available for agent
${MemberChannel})
; *** This should all be on a single line
; playback a message stating the channel has not yet been assigned
same => n,Playback(silence/1&channel¬-yet-assigned)
same => n,Hangup()
[ACD]
;
; Used for logging agents into all configured queues per the AstDB
;
;
; Logging into multiple queues via the AstDB system
exten => *54,1,Verbose(2,Logging into multiple queues per the database values)
; get the available queues for this channel
same => n,GoSub(subSetupAvailableQueues,start,1())
same => n,Set(QueueCounter=1) ; setup a counter variable
; using CUT(), get the first listed queue returned from the AstDB
; Note that we’ve used ‘^’ as our delimiter
same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
; While the WorkingQueue channel variable contains a value, loop
same => n,While($[${EXISTS(${WorkingQueue})}])
; AddQueueMember(queuename[,interface[,penalty[,options[,membername
; [,stateinterface]]]]])
; Add the channel to a queue, setting the interface for calling
; and the interface for monitoring of device state
;
; *** This should all be on a single line
same => n,AddQueueMember(${WorkingQueue},${MemberChanType}/
${MemberChannel},,,${MemberChanType}/${MemberChannel})
same => n,Set(QueueCounter=$[${QueueCounter} + 1]) ; increase our counter
; get the next available queue; if it is null our loop will end
same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
same => n,EndWhile()
; let the agent know they were logged in okay
same => n,Playback(silence/1&agent-loginok)
same => n,Hangup()
exten => no_queues_available,1,Verbose(2,No queues available for ${MemberChannel})
same => n,Playback(silence/1&channel¬-yet-assigned)
same => n,Hangup()
; ————————-
; Used for logging agents out of all configured queues per the AstDB
exten => *56,1,Verbose(2,Logging out of multiple queues)
; Because we reused some code, we’ve placed the duplicate code into a subroutine
same => n,GoSub(subSetupAvailableQueues,start,1())
same => n,Set(QueueCounter=1)
same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
same => n,While($[${EXISTS(${WorkingQueue})}])
same => n,RemoveQueueMember(${WorkingQueue},${MemberChanType}/${MemberChannel})
same => n,Set(QueueCounter=$[${QueueCounter} + 1])
same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
same => n,EndWhile()
same => n,Playback(silence/1&agent-loggedoff)
same => n,Hangup()
; ————————-
; Used for pausing agents in all available queues
exten => *72,1,Verbose(2,Pausing member in all queues)
same => n,GoSub(subSetupAvailableQueues,start,1())
; if we don’t define a queue, the member is paused in all queues
same => n,PauseQueueMember(,${MemberChanType}/${MemberChannel})
same => n,GotoIf($[${PQMSTATUS} = PAUSED]?agent_paused,1:agent_not_found,1)
exten => agent_paused,1,Verbose(2,Agent paused successfully)
same => n,Playback(silence/1&unavailable)
same => n,Hangup()
; ————————-
; Used for unpausing agents in all available queues
exten => *87,1,Verbose(2,UnPausing member in all queues)
same => n,GoSub(subSetupAvailableQueues,start,1())
; if we don’t define a queue, then the member is unpaused from all queues
same => n,UnPauseQueueMember(,${MemberChanType}/${MemberChannel})
same => n,GotoIf($[${UPQMSTATUS} = UNPAUSED]?agent_unpaused,1:agent_not_found,1)
exten => agent_unpaused,1,Verbose(2,Agent paused successfully)
same => n,Playback(silence/1&available)
same => n,Hangup()
; ————————-
; Used by both pausing and unpausing dialplan functionality
exten => agent_not_found,1,Verbose(2,Agent was not found)
same => n,Playback(silence/1&cannot-complete-as-dialed)
You could further refine these login and logout routines to take into account that the AQMSTATUS and RQMSTATUS channel variables are set each time AddQueueMember() and RemoveQueueMember() are used. For example, you could set a flag that lets the queue member know he has not been added to a queue by setting a flag, or even add recordings or text-to-speech systems to play back the particular queue that is producing the problem. Or, if you’re monitoring this via the Asterisk Manager Interface, you could have a screen pop, or use JabberSend() to inform the queue member via instant messaging.
An Introduction to Device State
Device states in Asterisk are used to inform various applications as to whether your device is currently in use or not. This is especially important for queues, as we don’t typically want to send callers to an agent who is already on the phone. Device states are controlled by the channel module, and in Asterisk only chan_sip has the appropriate handling. When the queue asks for the state of a device, it first queries the channel driver (e.g., chan_sip). If the channel cannot provide the device state directly (as is the case
with chan_iax2), it asks the Asterisk core to determine it, which it does by searching through channels currently in progress.
In order to correctly determine the state of a device in Asterisk, we need to enable call counters in sip.conf. By enabling call counters, we’re telling Asterisk to track the active calls for a device so that this information can be reported back to the channel module and the state can be accurately reflected in our queues. First, let’s see what happens to our queue without the callcounter
option:
*CLI> queue show support
support has 0 calls (max unlimited) in ‘rrmemory’ strategy
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
Members:
SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet
No Callers
Now suppose we have an extension in our dialplan, 555, that calls MusicOnHold(). If we dial that extension without having enabled call counters, a query of the support queue (of which SIP/0000FFFF0001 is a member) from the Asterisk CLI will show
something similar to the following:
— Executing [555@LocalSets:1] MusicOnHold(“SIP/0000FFFF0001-00000000”,
“”) in new stack
— Started music on hold, class ‘default’, on SIP/0000FFFF0001-00000000
*CLI> queue show support
support has 0 calls (max unlimited) in ‘rrmemory’ strategy
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
Members:
SIP/0000FFFF0001 (dynamic) (Not in use) has taken no calls yet
No Callers
Notice that even though our phone should be marked as In Use because it is on a call, it does not show up that way when we look at the queue status. This is obviously a problem since the queue will consider this device as available, even though it is already on a call.
To correct this problem, we need to add callcounter=yes to the [general] section of our sip.conf file. We can also specifically configure this for any template or peer (since it is a peer-level configuration option); however, this is really something you’ll want to
set for all peers that might ever be part of a queue, so it’s normally going to be best to put this option in the [general] section, or as part of each template.
Edit your sip.conf file so it looks similar to the following:
[general]
context=unauthenticated ; default context for incoming calls
allowguest=no ; disable unauthenticated calls
srvlookup=yes ; enabled DNS SRV record lookup on outbound calls
udpbindaddr=0.0.0.0 ; listen for UDP request on all interfaces
tcpenable=no ; disable TCP support
callcounter=yes ; enable device states for SIP devices
Then reload the chan_sip module and perform the same test again:
*CLI> sip reload
Reloading SIP
== Parsing ‘/etc/asterisk/sip.conf’: == Found
The device should now show In use when a call is in progress from that device:
== Parsing ‘/etc/asterisk/sip.conf’: == Found
== Using SIP RTP CoS mark 5
— Executing [555@LocalSets:1] MusicOnHold(“SIP/0000FFFF0001-00000001”,
“”) in new stack
— Started music on hold, class ‘default’, on SIP/0000FFFF0001-00000001
*CLI> queue show support
support has 0 calls (max unlimited) in ‘rrmemory’ strategy
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
Members:
SIP/0000FFFF0001 (dynamic) (In use) has taken no calls yet
No Callers
In short, Queue() needs to know the state of a device in order to properly manage call distribution. The callcounter option in
sip.conf is an essential component of a properly functioning queue.