Все хоть раз слышал “Извините, в данный момент все операторы заняты” знайте это работа Automatic Call Distribution (ACD) Queues. Давайте сделаем и себе такое)
Для построения call-центра нам понадобится Астериск и логика. Следует уяснить раз и навсегда, когда абонент звонит в поддержку, он хочет получить помощь от ЧеЛоВеКа!!!!111, и пофигу на Ваше красивое голосовое меню)
Для начала стоит уяснить некоторые понятия:
member – это телефон или другое устройство
agent – оператор, который обрабатывает звонки
В принципе эти два понятия схожи, и с некоторой оговоркой их можно воспринимать как синонимы.
Переходим к созданию очередей, правим queues.conf
Создадим 2 очереди, с помощью шаблона, [sales] и [support].
[general] autofill=yes ; распределять всех ожидающих абонентов, по доступным операторам shared_lastcall=yes ; дать паузу оператору,который залогинен в нескольких очередях, ; после последнего вызова например закрытия заявки в теркере. [StandardQueue](!) ; Шаблон для создания очередей musicclass=default ; устанавливаем музыку во время ожидания strategy=rrmemory ; Стратегия распределения звонков между операторами ; напр(Round Robin Memory) joinempty=no ; не включать абонентов в очередь если в ней не ; зарегистрировано ни одного оператора leavewhenempty=yes ; покинуть очередь если нет доступных операторов ringinuse=no ; не звенеть оператору если статус InUse. ; Оператор не может распылятся на 100500 абонентов [sales](StandardQueue) ; создаем очередь на основе шаблона [support](StandardQueue) ; создаем очередь на основе шаблона
Перезагрузить модуль:
*CLI> module reload app_queue.so
Проверка очередей:
*CLI> queue show
Добавить оператора с помощью CLI:
*CLI> queue add member <channel> to <queue> [[[penalty <penalty>] as <membername>] state_interface <interface>]
где channel это канал который подкл, например SIP/0000FFFF0003
queue имя очереди, например support или sales
Удаление оператора через CLI
*CLI> queue remove member SIP/0000FFFF0001 from support Removed interface 'SIP/0000FFFF0001' from queue 'support'
С помощью CLI(queue pause member и queue unpause member ) можно также поставить на паузу/снять с паузы оператора
*CLI> queue pause member SIP/0000FFFF0001 queue support reason DoingCallbacks paused interface 'SIP/0000FFFF0001' in queue 'support' for reason 'DoingCallBacks'
*CLI> queue unpause member SIP/0000FFFF0001 queue support reason off-break unpaused interface 'SIP/0000FFFF0001' in queue 'support' for reason 'off-break'
Параметр penalty: Очередь вызовов могут обрабатывать люди с разными обязанностями и, следовательно, у одних это обязанность основная , а у других – второстепенная, и основную нагрузку по приему вызовов нам надо возложить на одних людей, а для подстраховки – других (к примеру, если у нас есть очередь, куда поступают вызовы от клиентов, которые хотят что-то купить, то основная обязанность ее обработки ложиться на менеджеров, следовательно, их определяем без пенальти. Еще некоторые люди могут принимать звонки, если у менеджеров полный завал, с пенальти 1, и, с пенальти 2, мы можем определить агентов совсем уж не относящихся к продажам, например, телефон в службе технической поддержки).
Правим extensions.conf для возможности подключить очередь
[Queues] exten => 7001,1,Verbose(2,${CALLERID(all)} entering the support queue) same => n,Queue(support) same => n,Hangup() exten => 7002,1,Verbose(2,${CALLERID(all)} entering the sales queue) same => n,Queue(sales) same => n,Hangup()
Для управления входом/выходом операторов из очереди, с помощью диалплана, будем использовать приложения:
AddQueueMember()
RemoveQueueMember()
А также возможность временного выхода из очереди(пауза)
PauseQueueMember()
UnpauseQueueMember()
Добавим этот функционал в диалплан
[QueueMemberFunctions] exten => *54,1,Verbose(2,Logging In Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,AddQueueMember(support,${MemberChannel}) ; ${AQMSTATUS} ; ADDED ; MEMBERALREADY ; NOSUCHQUEUE exten => *56,1,Verbose(2,Logging Out Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,RemoveQueueMember(support,${MemberChannel}) ; ${RQMSTATUS}: ; REMOVED ; NOTINQUEUE ; NOSUCHQUEUE exten => *72,1,Verbose(2,Pause Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,PauseQueueMember(support,${MemberChannel}) ; ${PQMSTATUS}: ; PAUSED ; NOTFOUND exten => *87,1,Verbose(2,Unpause Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,UnpauseQueueMember(support,${MemberChannel}) ; ${UPQMSTATUS}: ; UNPAUSED ; NOTFOUND
Опции секции [general] в queues.conf
Опция | Значение | Описание |
---|---|---|
persistentmembers | yes, no | Для хранения динамически добавляемого оператора в AstDB так что они могут быть вновь добавлены после перезагрузки Asterisk |
autofill | yes, no | Распределять всех ожидающих абонентов, по доступным операторам |
monitor-type | MixMonitor, незадано | Задает приложение для записи разговоров |
updatecdr | yes, no | Запишет в CDR в dstchannel имя оператора, |
shared_lastcall | yes, no | Позволяет Дать паузу оператору,который залогинен в нескольких очередях, после последнего вызова например закрытия заявки в теркере, поскольку его последний звонок учитывается во всех очередях |
Опции доступные при описании очереди:
Опция | Значение | Описание |
---|---|---|
musicclass | Класс MOH описанный в musiconhold.conf | Устанавливает класс MOH для конкретной очереди. Может быть переназначено с помощью переменной CHANNEL(musicclass) |
announce | Имя файла анонса | Этот файл воспроизводится агенту, который принимает звонок |
strategy | ringall, leastrecent, fewestcalls, random, rrmemory, linear, wrandom | ringall – звонить всем доступным операторам (по умолчанию) leastrecent: звонить оператору который дольше всех не отвечал на звонки. fewestcalls: оператору обработавшему наименьшее к-во звонков random: произвольному оператору rrmemory: по кругу(round-robin) linear: в заданном порядке, всегда обрабатывается с начала списка, первый будет работать не разгибая спины) wrandom: произвольному оператору, но используя penalties как веса. |
servicelevel | значение в сек | Для определения уровня обслуживания, отвечен ли звонок вовремя |
context | Dialplan context | Позволяет звонящему выйти из очереди, с помощью одного DTMF символа |
penaltymemberslimit | больше 0 | Для пренебрежения penalty, если значение операторов ниже заданного значения |
timeout | Значение в сек | Время сколько звенеть оператору) |
retry | Значение в сек | Определяет время ожидания перед попыткой следующего оператора в очереди, при истечении таймаута предыдущего оператора |
timeoutpriority | app, conf | Контролирует 2 возможных таймаута, для оператора и глобальный для всей очереди(app), кому отдать приоритет |
weight | Больше или равно 0 | Подразумевает приоритет вызова, для гарантии того, что вызов, ожидающий в очереди с более высоким приоритетом, будет обработан одним из первых. Будет задерживаться обработка вызовов только для менее приоритетных очередей, если оператор, который обрабатывает очередь, уже занимается вызовом из более высокоприоритетной очереди. |
wrapuptime | Значение в секундах | значение минимального промежутка времени, с момента, когда работа с абонентом завершена и до того, как участник обработки может принять новый вызов из очереди. |
autofill | yes, no | Распределять всех ожидающих абонентов, по доступным операторам |
autopause | yes, no, all | Вкл/выкл автопаузу для операторов, кто не принял вызов. all автопауза для всех очередей, в каких присутствует данный оператор. |
maxlen | Больше или равно 0 | Максимальное число ожидающих в очереди, 0-неограниченно |
setinterfacevar | yes, no | Если yes, то следующие переменные будут заданы до установки связи звонящего и оператора: MEMBERINTERFACE: Интерфейс оператора, например: Agent/1234 MEMBERNAME: имя оператора MEMBERCALLS: количество принятых вызовов MEMBERLASTCALL: время последнего вызова MEMBERPENALTY: penalty оператора MEMBERDYNAMIC: динамически ли был добавлен оператор или нет MEMBERREALTIME: оператор был добавлен из real time или нет |
setqueueentryvar | yes, no | Если yes, то следующие переменные будут установлены до соединения: QEHOLDTIME: время ожидания в очереди QEORIGINALPOS: позиция какую первоночально занимает звонящий |
setqueuevar | yes, no | Если yes, то следующие переменные будут установлены до соединения: QUEUENAME: имя очереди QUEUEMAX: максимальное количество звонков разрешенное в этой очереди QUEUESTRATEGY: стратегия распределения вызовов в очереди QUEUECALLS: количество звонков в очереди в текущий момент QUEUEHOLDTIME: среднее время ожидания в очереди QUEUECOMPLETED: к-во завершенных звонков в очереди QUEUEABANDONED: к-во неуспешных звонков QUEUESRVLEVEL: уровень обслуживания в очереди QUEUESRVLEVELPERF: текущий уровень обслуживания |
membermacro | Имя макроса определенного в диалплане | Макрос будет выполнен при соединении оператора со звонящим |
announce-frequency | Значение в секундах | Как часто абоненту, ожидающему в очереди вызовов, сообщать его позицию и/или приблизительное время ожидания обработки его вызова (0=выключено) |
min-announce-frequency | Значение в секундах | Минимальный промежуток времени между анонсами |
periodic-announce-frequency | Значение в секундах | Интервал периодических анонсов |
random-periodic-announce | yes, no | Будет играть определенный анонс в произвольном порядке |
relative-periodic-announce | yes, no | Если yes, periodic-announce-frequency таймер начнет отсчет во время окончания воспроизведения записи, вместо того чтобы начать сначала. По умолчанию нет. Например у нас установлено воспроизводить запись каждые 45 секунд, а продолжительность записи 30 секунд, если стоит no, то через 15 секунд снова начнется воспроизведение |
announce-holdtime | yes, no, once | Играть ли время ожидания во время периодических анонсов |
announce-position | yes, no, limit, more | yes – воспроизводить позицию no – не воспроизводить позицию limit – если позиция в очереди в пределах announce-position-limit more – если позиция в очереди превышает announce-position-limit |
announce-position-limit | Больше равно 0 | используется в связке с предыдущей опцией |
announce-round-seconds | К-во секунд | Анонсим приблизительное(округленное) к-во секунд, например вместо “1 минута и 23 секунды” мы округляем до ближайшего 30сек интервала, в данном случае будет 1 минута 30 секунд. |
queue-thankyou | Имя файла | Если не указано, то играет “Спасибо за оказанное внимание.” если пусто, то не играет ничего |
queue-youarenext | Имя файла | Если не задано играет “Ваш вызов в настоящий момент первый в очереди. Оставайтесь на линии, вас соединят с первым освободившимся оператором.”, если пусто, то не играет ничего |
queue-thereare | Имя файла | Если не задано играет “В настоящий момент вы находитесь в очереди…”, если пусто, то не играет ничего |
queue-callswaiting | Имя файла | Если не задано играет “Ожидайте ответа.”, если пусто, то не играет ничего |
queue-holdtime | Имя файла | Если не задано играет “Оставшееся время ожидания в очереди составляет”, если пусто, то не играет ничего |
queue-minutes | Имя файла | Если не задано играет “минут”, если пусто, то не играет ничего |
queue-seconds | Имя файла | секунд |
queue-reporthold | Имя файла | Время ожидания… |
periodic-announce | Имя файла | Если не задано, то играет “В настоящий момент все операторы заняты. Пожалуйста, оставайтесь на линии, вас соединят с первым освободившимся оператором” |
monitor-format | gsm, wav, wav49, допустимый формат файлов | Формат записи диалогов |
monitor-type | MixMonitor, не задано | Задает приложение для записи разговоров, если не задано используется Monitor |
joinempty | paused, penalty, inuse, ringing, unavailable, invalid, unknown, wrapup | Опции могут быть перечислены через запятую, при каких считать оператора недоступным |
leavewhenempty | paused, penalty, inuse, ringing, unavailable, invalid, unknown, wrapup | Удалять все вызовы, поступившие в очередь, у которой нет участников или агентов, для ее обработки, задаются условия при которых считать что оператор недоступен |
eventwhencalled | yes, no, vars | yes следующие события будут переданы в (AMI): AgentCalled AgentDump AgentConnect AgentComplete vars – все переменные канала ассоциированные с агентом будут также переданы в AMI. |
eventmemberstatus | yes, no | yes – QueueMemberStatus будет передано в AMI. !!Может сгенерировать много менеджмент событий |
reportholdtime | yes, no | Обьявлять время ожидания до соединения |
ringinuse | yes, no | Не звонить оператору который уже разговаривает, работает только на SIP |
memberdelay | Знач в сек | время задержки между моментом, когда агент отвечает на вызов и соединения его с вызывающим абонентом |
timeoutrestart | yes, no | Если параметр “timeoutrestart” установлен в значение yes, тогда значение таймаута для агента будет сбрасываться, если от него будет получен сигнал BUSY или CONGESTION. Это бывает полезно, когда агент имеет возможность отметить вызов, отвергая его или, производя некоторые действия, которые имеют подобный эффект. (Обнаружено, что если вызов агента завершился со статусом NOANSWER (ring, no-answer), это так же заставляет вызов отправить к следующему агенту в очереди по алгоритму roundrobin). |
defaultrule | Правило в queuerules.conf | Ассоциирует правило очереди в queuerules.conf к данной очереди, которое будет использоваться для динамического изменения мин и макс penalties, которая используется для выбора доступных агентов |
member | Устройство | Для статического задания оператора в очереди. Technology/Device_ID Пример: Agent/1234, SIP/0000FFFF0001, DAHDI/g0/14165551212). |
Приоритет очереди
Бывает так, что нужно перевести звонящего из одной очереди в другую, но с наименьшим временем ожидания, поскольку человек уже достаточно наобщался(наждался) в предыдущей очереди), для этого используется опция weight.
Например есть 2 очереди с разными weight: support и support-priority.
Включив агентов в обе очереди можем динамически распределять нагрузку, но есть одно но, пока более приоритетная очередь не очистится, звонки в менее приоритетной будут без ответа, поэтому делают иерархию операторов, некоторые отвечают на звонки сугубо менее приоритетной очереди.
С помощью приоритетов мы задаем операторов, которые в случае чего придут на выручку основным операторам очереди, задается следующим образом:
Для трех очередей
[support](StandardQueue) member => SIP/0000FFFF0001,0,James Shaw ; preferred member => SIP/0000FFFF0002,10,Kay Madsen ; second preferred member => SIP/0000FFFF0003,20,Danielle Roberts ; least preferred [sales](StandardQueue) member => SIP/0000FFFF0002,0,Kay Madsen member => SIP/0000FFFF0003,10,Danielle Roberts member => SIP/0000FFFF0001,20,James Shaw [billing](StandardQueue) member => SIP/0000FFFF0003,0,Danielle Roberts member => SIP/0000FFFF0001,10,James Shaw member => SIP/0000FFFF0002,20,Kay Madsen
Из диалплана приоритеты задаются с помощью AddQueueMember
; AddQueueMember(queuename[,interface[,penalty[,options[,membername ; [,stateinterface]]]]]) same => n,AddQueueMember(support,${MemberChannel},${QueuePenalty},,${MemberName})
Используя queuerules.conf можно динамически управлять Penalties с помощью переменных QUEUE_MIN_PENALTY и QUEUE_MAX_PENALTY. Можно указать какие операторы, с какими Penalties могут принимать звонки.
Изменяем минимальный penalty до 1 максимальный до 5 если звонящий ждет более 60 секунд.
[more_members] penaltychange => 60,5,1 ; ;можно еще так ;после 30 сек увеличиваем макс penalty на 1(станет 5) ;после 45 сек уменьшаем мин penalty на 1(станет 1) ; итд... penaltychange => 30,+1 penaltychange => 45,,-1 penaltychange => 60,+1 penaltychange => 120,+2
Проверить правила:
*CLI> queue show rules Rule: more_members After 60 seconds, adjust QUEUE_MAX_PENALTY to 5 and adjust QUEUE_MIN_PENALTY to 1
*CLI> queue show rules Rule: more_members After 30 seconds, adjust QUEUE_MAX_PENALTY by 1 and adjust QUEUE_MIN_PENALTY by 0 After 45 seconds, adjust QUEUE_MAX_PENALTY by 0 and adjust QUEUE_MIN_PENALTY by -1 After 60 seconds, adjust QUEUE_MAX_PENALTY by 1 and adjust QUEUE_MIN_PENALTY by 0 After 120 seconds, adjust QUEUE_MAX_PENALTY by 2 and adjust QUEUE_MIN_PENALTY by 0
Изменим наш Диалплан, чтобы он подхватывал правила изменения:
[Queues] exten => 7000,1,Verbose(2,Entering the support queue) same => n,Set(QUEUE_MIN_PENALTY=2) ; set minimum queue member penalty same => n,Set(QUEUE_MAX_PENALTY=4) ; set maximum queue member penalty ; Queue(queuename[,options[,URL[,announceoverride[,timeout[,AGI[,macro ; [,gosub[,rule[,position]]]]]]]]]) same => n,Queue(support,,,,,,,,more_members) ; entering queue with minimum and ; maximum member penalties
Таймеры:
Всякое бывает и может так случится, что звонок из очереди по таймауту выйдет в диалплан. Рассмотрим случай
Абсолютный таймаут = 10 сек,
Длительность звонка оператору = 5сек
Пауза между попытками позвонить другому оператору= 4сек.
Мы звоним 1му оператору 5 секунд, потом ждем 4 секунды, и ….
А) звоним след оператору 1 секунду и выходим
Б) звоним полные 5 секунд оператору и выходим
Это задается с помощью timeoutpriority.
Случай А) = app
Случай Б) = conf
Local channels
Для “более лучшей” гибкости можно использовать local channels
Добавляется аналогично
member => Local/SIP-0000FFFF0001@MemberConnector
Следует описать контекст MemberConnector
[MemberConnector] exten => _[A-Za-z0-9].,1,Verbose(2,Connecting ${CALLERID(all)} to Agent at ${EXTEN}) ; filter out any bad characters, allowing alphanumeric characters and the hyphen same => n,Set(QueueMember=${FILTER(A-Za-z0-9\-,${EXTEN}) ; assign the first field of QueueMember to Technology using the hyphen separator same => n,Set(Technology=${CUT(QueueMember,-,1)}) ; assign the second field of QueueMember to Device using the hyphen separator same => n,Set(Device=${CUT(QueueMember,-,2)}) ; dial the agent same => n,Dial(${Technology}/${Device}) same => n,Hangup()
При использовании, Local channel как оператора, Queue() не обязательно знать состояние звонка, особенно если локальный канал оптимизирован. Queue будет мониторить состояние локального канала, а не реальное устройство.
Но мы можем передать в queue() реальное устройство(только SIP):
[support](StandardQueue) member => Local/SIP-0000FFFF0001@MemberConnector,,,SIP/0000FFFF0001
Добавление/удаление оператора с локальным каналом:
[QueueMemberLogin] exten => 500,1,Verbose(2,Logging in device ${CHANNEL(peername)} into the support queue) ; Save the device's technology to the MemberTech channel variable same => n,Set(MemberTech=${CHANNEL(channeltype)}) ; Save the device's identifier to the MemberIdent channel variable same => n,Set(MemberIdent=${CHANNEL(peername)}) ; Build up the interface name and assign it to the Interface channel variable same => n,Set(Interface=${MemberTech}/${MemberIdent}) ; Add the member to the support queue using a Local channel. We're using the same ; format as before, separating the technology and the device indentifier with ; a hyphen and passing that information to the MemberConnector context. We then ; use the IF() function to determine if the member's technology is SIP and, if so, ; to pass back the contents of the Interface channel variable as the value to the ; state interface field of the AddQueueMember() application. ; ; *** This line should not have any line breaks same => n,AddQueueMember(support,Local/${MemberTech}-${MemberIdent} @MemberConnector,,,${IF($[${MemberTech} = SIP]?${Interface})}) same => n,Playback(silence/1) ; Play back either the agent-loginok or agent-incorrect file, depending on what ; the AQMSTATUS variable is set to. same => n,Playback(${IF($[${AQMSTATUS} = ADDED]?agent-loginok:agent-incorrect)}) same => n,Hangup()
Лог очереди хранится в /var/log/asterisk/queue_log
Параметры задаются в logger.conf
queue_log Включено или нет логирование очереди (по умолчанию yes).
queue_log_to_file нужно ли писать в файл даже если включена поддержка реалтайма(по умолчанию no).
queue_log_name имя файла. По умолчанию queue_log.
queue_log это разделенный пайпами | список ссобытий. Поля в файле:
Дата|UID звонка|имя очереди|Имя соединенного канала|тип события|параметры события(опционально)
1292284222|MANAGER|7100|Local/9990@MemberConnector|REMOVEMEMBER| 1292284222|MANAGER|7200|Local/9990@MemberConnector|REMOVEMEMBER| 1292284491|MANAGER|7200|Local/9990@MemberConnector|ADDMEMBER| 1292284519|psy1-1292284515.93|7100|NONE|ENTERQUEUE||4165551212|1 1292284519|psy1-1292284515.93|7100|Local/9996@MemberConnector|RINGNOANSWER|0 1292284521|psy1-1292284515.93|7100|Local/9990@MemberConnector|CONNECT|2 |psy1-1292284519.96|0 1292284519|psy1-1292284515.93|7100|NONE|ENTERQUEUE||4165551212|1
ID Может быть не уникальным. В некоторых случаях АМI может чет делать, в таких случаях появляются записи с MANAGER
Более детально о queues realtime Тут