Еще в 2014 году в Viber добавили поддержку бесплатных исходящих звонков на номера сети iNum. Что такое iNum? iNum (international Number) - международный номер. Данные номера позиционируются как глобальные телефонные номера, не зависящие от географической привязки, текущего местоположения, расстояния и государственных границ. iNum использует телефонный код +883.
Что нужно для подключения номера +883? Самый простой способ - зарегистрироваться на zadarma или youmagic, либо другого SIP провайдера, который предоставляет такие номера. Подключаем номер +883 к Webitel и теперь клиенты могут бесплатно звонить с Viber к нам:
Наша задача на сегодня - сразу после того, как мы поставим абонента в очередь ожидания, озвучить среднее время до соединения с оператором.
Для решения задачи мы воспользуемся приложением CDR, которое позволит нам выполнить запрос в базу elasticsearch и получить среднее время ожидания абонентов в очереди main за последний час. Правильный запрос в elasticsearch будет иметь вот такой вид:
Остается оформить все в нашей схеме ACR: получить целую часть из среднего значения ожидания в очереди, текст озвучим с помощью синтеза речи и т.д.
Привожу простой пример такой схемы:
[ { "cdr": { "exportVar": { "avg_wait": "aggregations.waiting.value" }, "elastic": { "aggs": { "waiting": { "avg": { "field": "queue.wait_duration" } } }, "index": "cdr-a*", "limit": 0, "query": "*", "filter": { "bool": { "must": [ { "match": { "queue.name": "main" } }, { "range": { "created_time": { "gte": "now-1h", "lte": "now" } } } ] } } } } }, { "math": { "data": "${avg_wait}", "setVar": "avg_wait", "fn": "ceil" } }, { "log": "avg_wait: ${avg_wait}" }, { "queue": { "name": "main", "timer": { "interval": 1, "tries": 1, "actions": [ { "ccPosition": { "var": "ccPosition" } }, { "if": { "expression": "!${avg_wait} || ${avg_wait} < 60", "then": [ { "tts": { "text": "Расчетное время ожидания до соединения с оператором меньше одной минуты.", "voice": "Maxim" } } ] } }, { "if": { "expression": "${avg_wait} > 120", "then": [ { "tts": { "text": "Расчетное время ожидания до соединения с оператором больше двух минут.", "voice": "Maxim" } } ], "else": [ { "tts": { "text": "Расчетное время ожидания до соединения с оператором меньше двух минут.", "voice": "Maxim" } } ] } } ] } } } ]
Довольно часто наши клиенты сталкиваются с необходимостью выгрузить информацию о звонках из раздела Call Detail Record к себе. Как вы уже знаете (даже если взять во внимание предыдущую запись в этом блоге, а именно 📲 Липкость звонка), вся историческая статистика хранится в базе elasticsearch. В документации вы так же могли увидеть пример получения данных с помощью нашего REST API. Но, что если звонков несколько сотен? Или несколько тысяч? Как правильно получить такой объем данных? Сегодня я расскажу как работать с большими объемами данных.
Scroll
Для получения большого количества данных, в elasticsearch предусмотрен функционал scroll, который мы повторили и в нашем REST API. Рассмотрим на примере:
{ "scroll" : "5m", "limit": 1000, "sort": { "created_time": { "order": "desc", "unmapped_type": "boolean" } }, "index": "cdr-a", "query": "*", "columns": [ "created_time", "uuid", "direction", "duration" ], "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/w", "lte": "now" } } } ] } } ] }
В тело нашего запроса мы добавили 2 новых параметра:
- scroll - как долго на сервере держать результат запроса
- limit - какими порциями возвращать результат запроса
Дальше, выполняем первый запрос с указанным телом на REST API, для простоты я использую консольную утилиту cURL:
curl -s -L -XPOST \ -H 'Content-Type: application/json' \ -H 'X-Access-Token: ciOiJIUzI1NiJ9.jEyM2UxNThjLWVkNzMtNDAwi'\ "https://pre.webitel.com/engine/api/v2/cdr/text" -d@webitel_scroll_request.json
Вместе с результатом, который не будет превышать заданного в limit значения, мы получим _scroll_id:
Теперь все последующие запросы мы выполняем уже с scrollId в теле запроса. Пример:
curl -s -L -XPOST \ -H 'Content-Type: application/json' \ -H 'X-Access-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6I9.IjKpitL05OLjUPeUQyd4E'\ "https://pre.webitel.com/engine/api/v2/cdr/text/scroll" -d ' { "scroll": "5m", "scrollId": 'МНОГО_БУКВ_ПОЛУЧЕННОГО_ИД' }'
Повторяем запрос, пока не заберем все данные с сервера.
Бонус
В качестве бонуса, подготовил небольшой bash скрипт, который с помощью cURL и jq поможет вам выкачать необходимые данные и сохранить в CSV файл:
Тело запроса должно находится в файле webitel_scroll_request.json в возле данного скрипта.
Удачи с запросами!
Использование IVR меню, в большинстве случаев, позволяет эффективно распределять входящие звонки и снижать нагрузку с секретаря. Клиент звонит, выбирает нужный отдел либо донабирает внутренний номер сотрудника и достигает цели. Но, как быть в ситуации, когда клиент не знает кому он звонит? Что если это наш менеджер не смог дозвонится клиенту, который перезвонил и попал на IVR? В данном случае будет полезным функционал "Липкости звонка". Мы уже рассматривали реализацию: Входящий звонок с маршрутизацией на ответственного в bpm'online. Сегодня мы рассмотрим пример маршрутизации в первую очередь на того, кто сегодня уже общался с данным номером либо звонил ему. Приступим!
Kibana нам в помощь
Для решения поставленной задачи нам понадобится приложение cdr, которое предназначено для поиска по журналу звонков в elasticsearch. В начале, нам нужно написать правильный запрос. Делаем тестовый звонок и открываем интерфейс Kibana, раздел Discover. Выбираем для отображения колонки extension (номер сотрудника), caller_id_number (номер абонента), destination (номер назначения).
Теперь сделаем запрос с фильтрацией по номеру абонента, он может быть либо в номере назначения (для исходящих) либо в номере абонента (для входящих):
destination_number:/0969716158/ OR caller_id_number:/0969716158/
Как результат, мы получаем номер сотрудника, extension, который общался либо звонил абоненту. И обязательно открываем Response (ответ со стороны elasticsearch), он нам пригодится ниже:
И так, как искать мы уже поняли, а теперь, добавим это все в маршрутизацию на стороне Webitel.
Запрос в CDR
Для работы с приложением cdr, нам нужно написать правильно запрос в elasticsearch
{ "limit": 1, "sort": { "created_time": { "order": "desc", "unmapped_type": "boolean" } }, "index": "cdr-a", "query": "*", "columns": [ "extension" ], "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/d", "lte": "now" } } }, { "query_string": { "query": "destination_number:/.*${caller_id_number}/ OR caller_id_number:/.*${caller_id_number}/", "analyze_wildcard": true, "default_field": "*" } } ] } } ] }
Разберем запрос:
Параметр | Описание |
---|---|
limit | Количество записей. 1 - так как нам нужно только последнего звонившего. |
sort | Сортировку делаем от новых к старым, опять же - только последний. |
index | В каком индексе осуществлять поиск: cdr-a |
columns | Какие поля возвращать - нам нужен номер сотрудника: extension |
query_string | Строка запроса в которой номер телефона заменен на канальную переменную caller_id_number |
range | Временной интервал: начиная с 00:00 текущего дня до теперь. |
А теперь посмотрим на ответ со стороны elasticsearh:
{ "took": 4, "timed_out": false, "_shards": { "total": 10, "successful": 10, "skipped": 0, "failed": 0 }, "hits": { "total": 5, "max_score": null, "hits": [ { "_index": "cdr-a-2019-.bpmonline.com", "_type": "cdr", "_id": "bb643481-310a-45cb-8c4e-95244c8e02b8", "_score": null, "fields": { "extension": [ "115" ] }, "sort": [ 1547032525216 ] } ] } }
Что бы добраться к нужному полю extension, мы должны пройти путь: hits => hits => fields => extension
Теперь давайте обновим нашу маршрутизацию в Public
Маршрутизация входящего звонка
У нас уже есть готовое IVR меню, так что мы добавим вначале проверку, и если результат не пустой, попытаемся соединить с нужным сотрудников (а с помощью userData добавим еще условие, что бы он был в статусе onhook - Готов). Если сотрудник не ответит в течение 15 секунд, тогда уже продолжим выполнять нашу типовую IVR схему:
["начало схемы"], { "cdr": { "exportVar": { "last_cid": "hits.hits.0.fields.extension.0" }, "elastic": { "limit": 1, "sort": { "created_time": { "order": "desc", "unmapped_type": "boolean" } }, "index": "cdr-a", "query": "*", "columns": [ "extension" ], "filter": [ { "bool": { "must": [ { "range": { "created_time": { "gte": "now/d", "lte": "now" } } }, { "query_string": { "query": "caller_id_number:/.*${caller_id_number}/ AND destination_number:${destination_number}", "analyze_wildcard": true, "default_field": "*" } } ] } } ] } } }, { "if": { "expression": "${last_cid}", "then": [ { "userData": { "name": "${last_cid}", "var": "account_state", "setVar": "acc_state" } }, { "log": "CID: ${caller_id_number}, Ext: ${last_cid} (${acc_state})" }, { "if": { "expression": "${last_cid} && ${acc_state} == 'onhook'", "then": [ { "bridge": { "endpoints": [ { "name": "${last_cid}", "type": "user", "parameters": [ "leg_timeout=15" ] } ] } } ] } } ] } }, ["продолжение схемы"]