close

Вход

Забыли?

вход по аккаунту

Барбашов Даниил Владиславович. Разработка программной системы интеграции и диспетчеризации каналов разнородных систем обмена сообщениями

код для вставки
WШИСТЕРСТВ О ОБРАЗО
ВАНИЯ
И НАУКИ РОССИЙСКОЙ ФЕ
ДЕРАЦИИ
ФЕ ДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТН
ОЕ ОБРАЗОВАТЕЛЬНОЕ
УЧРЕЖДЕНИЕ ВЫСШЕГО ОБРАЗОВАНИЯ
«ОРЛОВСКИЙ ГОСУДАРСТВЕШ-IЫЙ УНИВ
ЕРСИТЕТ
ИМЕНИ И.С. ТУРГЕНЕВА»
ВЫПУСКНАЯ К ВАЛИФИКАЦИОННАЯ РАБОТА
по направлению подготовки
09.03.04 Программная инженерия
Промышленная разработка программного обеспечения
Студента Барбашова Даниила Владиславовича
шифр 140192
Институт приборостроения, автоматизации и информационных технологий
Тема выпускной квалификационной работы
«Разработка программной системы интеграции и диспетчеризации каналов
разнородных систем обмена сообщениями»
Студент
Д.В. Барбашов
Руководитель
А .И. Фролов
Нормоконтроль
А.Ю. Ужаринский
Зав. кафедрой
программной инженерии
А.И. Фролов
Орёл 2018
МИНИСТЕРСТВО ОБР АЗОВ
АНИЯ И НАУ
КИ РОССИЙСКОЙ ФЕДЕРАЦИИ
ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТН
ОЕ ОБРАЗОВАТЕЛЬНОЕ
УЧРЕЖ,ЦВНИЕ ВЫСШЕГО ОБРАЗОВ
АНИЯ
«ОРЛОВСКИИ ГОСУДАРСТВЕННЫЙ УНИВЕР
СИТЕТ
ИМЕНИ И.С. ТУРГЕНЕВА»
Институт приборостроения, автоматизации и информационн
ых технологий
Кафедра информационных систем
09.03.04 Программная инженерия
Промышленная разработка программного обеспечения
УТВЕРЖДАЮ:
Зав. кафедрой
/4t�А.И.Фролов
/ >_____;:.:....:.=;..::___,.____ 20_& г.
ЗАДАНИЕ
на выполнение выпускной квалификационной работы
студента Барбашова Даниила Владиславовича
шифр 140192
1 Тема ВКР «Разработка программной системы интеграции и диспетчеризации
каналов разнородных систем обмена сообщениями»
Утверждена приказом по университету от «З 1» октября 2017г. № 2-3078
2 Срок сдачи студентом законченной работы «18» июня 2018г.
3 Исходные данные к работе
Теоретический материал; информация о предметной области
4 Содержание пояснительной записки (перечень подлежащих разработке
вопросов)
Анализ задачи разработки программной системы интеграции и диспетчеризации
каналов разнородных систем обмена сообщениями
Концептуальное проектирование программной системы интеграции и
диспетчеризации каналов разнородных систем обмена сообщениями
Детализированное проектирование программной системы интеграции и
диспетчеризации каналов разнородных систем обмена сообщениями
Особенности реализации программной системы интеграции и диспетчеризации
каналов разнородных систем обмена сообщениями
5 Перечень демонстрационного материала
Презентация, отображающая основные этапы и результаты выполнения Bl(p
Дата выдачи задания
« Jj_ » --'-иr!l��-- 201&' г.
Руководитель
А.И. Фролов
Задание принял к исполнению
Д.В. Барбашов
(подпись)
КАЛЕНДАРНЫЙ ПЛАН
Наименование этапов работы
Студент
Руководитель
Сроки выполнения
этапов аботы
Примечание
АННОТАЦИЯ
ВКР 94 с., 33 рис., 2 табл., 30 источников, 1 прил.
МЕССЕНДЖЕРЫ,
ОМНИКАНАЛЬНЫЕ
СИСТЕМЫ,
ОЧЕРЕДИ,
ИНТЕГРАЦИЯ КАНАЛОВ, IN-MEMORY DATABASES, NOSQL, REDIS,
ELASTICSEARCH.
Выпускная квалификационная работа посвящена разработке системы
интеграции и диспетчеризации каналов разнородных систем обмена
сообщениями.
В первой главе выпускной квалификационной работы приведено
описание
омниканальных
систем.
Проведен
анализ
алгоритмов
маршрутизации и диспетчеризации, способов интеграции. Перечислены и
проанализированы аналогичные решения. Выявлены функциональные и
нефункциональные требования к системе.
Во второй главе были разработаны функциональные спецификации к
системе. Разработана общая структура системы, выделены основные
подсистемы. Была составлена информационная модель системы, были
выделены потоки данных внутри и вне системы. Выделены основные классы
системы, составлена контекстная диаграмма классов.
В третьей главе составлена детализированная диаграмма классов, дано
детальное описание сущностей и их ролей в системе. Спроектированы
подсистема интеграции каналов, подсистема маршрутизации и подсистема
диспетчеризации. Также были спроектированы алгоритмы данных подсистем.
В четвертой главе представлена программная реализация основных
модулей системы, представлены примеры конфигурации разработанной
системы. Представлен пример реализации интеграции на основе канала EMail.
Графическая часть выпускной квалификационной работы включает
иллюстрации, таблицы, которые объединены в презентацию PowerPoint.
Библиографическая
часть
включает в себя 30 источников.
выпускной
квалификационной
работы
4
СОДЕРЖАНИЕ
ВВЕДЕНИЕ
6
1 АНАЛИЗ ЗАДАЧИ РАЗРАБОТКИ ПРОГРАММНОЙ СИСТЕМЫ
ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ РАЗНОРОДНЫХ
СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
8
1.1 Омниканальные системы
8
1.2 Виды интеграций и методы диспетчеризации разнородных каналов связи
10
1.3 Единый маршрут сообщений для каждого канала связи.
12
1.4 Обзор аналогов
14
1.4.1 Oktell Omnichannel
14
1.4.2 Омниканальная платформа онлайн-консультирования LiveTex
16
1.4.3 RedHelper
17
1.4.4 Jivosite
19
1.4.5 Naumen Оmni-Сhannel
20
2 КОНЦЕПТУАЛЬНОЕ ПРОЕКТИРОВАНИЕ ПРОГРАММНОЙ СИСТЕМЫ
ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ РАЗНОРОДНЫХ
СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
24
2.1 Разработка функциональных спецификаций
24
2.2 Общая структура разрабатываемой системы
26
2.3 Информационная модель системы
31
2.4 Контекстная диаграмма классов
36
3 ДЕТАЛИЗИРОВАННОЕ ПРОЕКТИРОВАНИЕ ПРОГРАММНОЙ
СИСТЕМЫ ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ
РАЗНОРОДНЫХ СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
39
3.1 Детализированная диаграмма классов
39
3.2 Проектирование подсистемы интеграции каналов
42
3.3 Проектирование подсистемы маршрутизации сообщений
46
3.4 Проектирование подсистемы диспетчеризации
49
3.5 Проектирование алгоритмов маршрутизации
52
5
3.6 Проектирование алгоритмов диспетчеризации
55
4 ОСОБЕННОСТИ РЕАЛИЗАЦИИ ПРОГРАММНОЙ СИСТЕМЫ
ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ РАЗНОРОДНЫХ
СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
59
4.1 Используемые библиотеки и технологии
59
4.2 Реализация интеграции канала на примере канала E-Mail
61
4.3 Особенности реализации модуля маршрутизации
63
4.4 Особенности реализации модуля диспетчеризации
65
ЗАКЛЮЧЕНИЕ
68
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ И ЛИТЕРАТУРЫ
69
ПРИЛОЖЕНИЕ А – ЛИСТИНГ ПРОГРАММЫ
72
УДОСТОВЕРЯЮЩИЙ ЛИСТ НА ДЕМОНСТРАЦИОННЫЙ МАТЕРИАЛ
92
ИНФОРМАЦИОННО-ПОИСКОВАЯ ХАРАКТЕРИСТИКА ДОКУМЕНТА
НА ЭЛЕКТРОННОМ НОСИТЕЛЕ
93
6
ВВЕДЕНИЕ
Современные технологии постоянно движутся вперед и в результате этого
прогресса у людей появляются новые возможности для общения. Развитие
интернета принесло людям новую технологию общения – мессенджеры. Идея
мессенджеров быстро набрала обороты и теперь на рынке существует довольно
много различных представителей этой концепции.
Мессенджеры задают тренд на мгновенное и удобное общение, каждая
популярная социальная сеть сегодня имеет свой встроенный мессенджер. У
пользователей возникает потребность перенести все коммуникации в выбранную
ими систему обмена сообщениями.
Чтобы поддержать имидж, бренды и компании стараются иметь свои
представительства
в
каждом
из
существующих
каналов.
Поддержание
современного образа компании хорошо сказывается на продажах, вызывает
доверие к бренду, создает ощущение что компания идет в ногу со временем.
Пользователи, в свою очередь, имеют возможность с удобством получать
интересующую информацию, техническую поддержку, а также последние
новости компании.
Для того чтобы поддерживать коммуникацию с пользователями во всех
средствах обмена сообщениями, необходимо распределить операторов по
каждому из мессенджеров. Это может быть довольно накладно, ведь управлять
большим
количеством
окон
одновременно
очень
сложно,
а
некоторые
мессенджеры в принципе не имеют версий приложений для персонального
компьютера.
Интерфейсы различных мессенджеров могут быть как схожими, так и в
корне отличаться друг от друга, поэтому обучение операторов нужно проводить в
каждом из средств общения.
В поддержке часто используется такой термин как обращение (или тикет).
Обращение
представляет
собой
осмысленный
диалог
с
пользователем,
ограниченный определенной тематикой, начинающийся с появившейся проблемы
7
у пользователя и оканчивающийся разрешением этой проблемы. В мессенджерах
же такое понятие отсутствует, диалог с пользователем представлен в виде
бесконечной ленты.
Отсутствует возможность сбора статистики, а даже если такая возможность
существует, то очень трудно составить общую статистику по всем мессенджерам.
Если использовать интерфейс мессенджеров, то у компании нет возможности
осуществлять мониторинг и оценку работы операторов.
Решение данных проблем может предоставить система, интегрирующая
различные каналы в одной системе. Цель данной выпускной квалификационной
работы – разработка системы, реализующей интеграцию и диспетчеризацию
каналов разнородных систем обмена сообщениями.
Для достижения поставленной цели необходимо выполнить следующие
задачи:
−
охарактеризовать
и
формализовать
задачу
интеграции
и
диспетчеризации каналов разнородных систем обмена сообщениями;
−
провести анализ предметной области и на основе этого составить
требования к системе;
−
разработать
спецификации
к
разрабатываемому
программному
обеспечению на основе поставленных требований;
−
разработать архитектуру и спроектировать структуру системы,
структуры данных и алгоритмы;
−
выбрать языковые и инструментальные средства разработки;
−
реализовать модули программного обеспечения, протоколы обмена
данными;
−
провести тестирование и отладку разработанной системы.
8
1 АНАЛИЗ ЗАДАЧИ РАЗРАБОТКИ ПРОГРАММНОЙ СИСТЕМЫ
ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ РАЗНОРОДНЫХ
СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
1.1 Омниканальные системы
Омниканальные
системы
–
это
системы,
интегрирующие
и
диспетчеризующие каналы разнородных систем обмена сообщениями. Благодаря
омниканальности таких систем, все коммуникации находятся в едином
интерфейсе. Становится легко оценить ситуацию, происходящую в компании,
сверху, поскольку все взаимодействия с клиентами находятся в единой системе.
Омниканальные системы являются ответом на новые потребности
пользователей и разрабатываются на основе последних технологических
решений. Разрозненные ранее каналы коммуникации объединяются вместе в
единый инструмент для обеспечения полного охвата аудитории компании и
взаимосвязанности.
В рамках омниканальности, для клиента нет значения каким образом с ним
связываются – он общается с брендом в целом, а не с «поддержкой по телефону»
или «отделом электронной почты», действующими автономно и часто дающими
противоречивую информацию [1].
Омниканальные системы дают хорошее подспорье для использования
технологии «больших данных» (Big Data) [2]. Действительно, удобство сбора
информации о клиентах, о его предпочтениях и намерениях в подобных системах
превышает известные аналоги, ведь вся информация поступает в систему через
единую шину данных.
Технология омниканальности – это новый инструмент, появившийся на
нашем рынке относительно недавно, около четырех лет назад, тем не менее
данному направлению посвящено несколько публикаций.
Так, развитию омниканальных сбытовых систем и их адаптации на
российском рынке посвящена статья И.А. Ипатьевой [3]. Автор в своей работе
отмечает
сформировавшийся
в
настоящее
время
высокий
спрос
на
9
омниканальную интеграцию. На сегодняшний день особенно актуальными стали
омниканальные продажи, которые удовлетворяют требования покупателей,
соответствуют современному технологическому уровню и фактически являются
единственной возможностью торговых компаний в обозримом будущем остаться
на рынке. При этом, для успешного внедрения этой системы компания должна
обеспечить идеальную отлаженную работу ERP-системы, CRM-системы, системы
управления складом, транспортом, web-платформы и, конечно же, безупречную
интеграцию всех элементов.
Омниканальность позволяет бизнесу сократить издержки на рабочую силу,
ведь при наличии единого удобного интерфейса и интеграций с ERP и CRM
системами, продуктивность операторов сильно возрастает. Взаимодействие
операторов и клиентов через каналы изображено на рисунке 1.1.
VK
Telegram
Система интеграции
и диспетчеризации
каналов
(омниканальная
система)
Viber
WebChat
E-Mail
SMS
Клиенты
…
Операторы
Каналы
Рисунок 1.1 – Взаимодействие операторов и клиентов через омниканальную
систему
Контактные
центры
также
испытывают
огромный
интерес
к
омниканальным системам [4]. Аналитики считают, что те контактные центры,
которые не научатся обслуживать клиентов с помощью различных каналов
коммуникаций, в ближайшие годы покинут рынок.
Помимо привычных каналов связи, таких как телефон и электронная почта,
на рынок выходят мессенджеры, и контактным центрам для того, чтобы
держаться на плаву, необходимо приспосабливаться к изменяющейся ситуации.
10
Клиенты ожидают, что смогут общаться с операторами привычным им образом.
Рост аудитории крупнейших мессенджеров показан на рисунке 1.2.
Рисунок 1.2 – Рост аудитории крупнейших мессенджеров, млрд. чел.
Молодые люди от 17 до 35 лет все чаще и охотнее выбирают в качестве
основного средства общения именно цифровые каналы связи. Цифровые каналы
являются для них самым быстрым и удобным способом общения. Бизнеспроцессы контактных центров выстроены для традиционного голосового
обслуживания, и теперь компании вынуждены следовать трендам и внедрять
новые методы общения с клиентами.
Клиент,
обслуживаемый
омниканальной
системой,
может
впервые
обратиться из одного канала, а потом продолжить общение с оператором из
другого, и все это благодаря бесшовной интеграции. Таким образом, растет
удовлетворенность клиента, контакт-центры успевают работать с большим
числом клиентов без потери качества, и при этом снижаются издержки.
1.2 Виды интеграций и методы диспетчеризации разнородных каналов связи
При создании омниканальной системы, на первое место встает вопрос
интеграции разнородных каналов связи с разрабатываемой системой.
Интеграция – это процесс объединения компонентов различных подсистем
в единую систему, таким образом, что получившаяся система способна
предоставить требуемый функционал. В процессе интеграции также необходимо
11
удостовериться, что подсистемы действуют вместе как единая скоординированная
система.
Интеграция может производиться с помощью большого количества
подходов, таких как настройка и проектирование компьютерных сетей,
интеграция
приложений
предприятия,
управление
бизнес-процессами
и
программирование.
Виды интеграций каналов омниканальной системы:
1)
Интеграция мессенджеров.
2)
Интеграция SMS.
3)
Интеграция Email.
В случае интеграции мессенджеров, вопрос выбора метода интеграции не
стоит, поскольку мессенджеры в основном предоставляют лишь один способ
интеграции – с помощью REST API.
При интеграции с каналом
SMS, существуют несколько методов
интеграции. Первый из них – подбор сервис, самостоятельно принимающего SMS
сообщений и предоставляющего «входящий ящик» посредством REST или SOAP
API. Второй вариант – это самостоятельная реализация устройства, принимающая
SMS сообщения и отправляющая эти сообщения в систему по выбранному
протоколу.
В случае интеграции с каналом Email можно использовать SMTP или POP3
клиент, в таком случае работа данной интеграции состоит в постоянном опросе
SMTP и POP3 серверов соответственно на предмет наличия новых сообщений.
Сообщения из интегрированных каналов преобразуются в единый формат, в
последующем
используемый
во
всех
интеракциях
внутри
системы.
Преобразованные сообщения поступают в единую очередь, где происходит
последующая обработка сообщений и подготовка обращений.
При разработке омниканальной системы возникает проблема назначения
обращений
клиентов
на
операторов.
Данный
процесс
называется
диспетчеризацией. Подразумевается, что обращение может быть в одно время
только у одного оператора. Если оператор отошел или вышел из сети, обращение
12
должно поступить на следующего свободного оператора. В случае, если
свободных операторов нет, то обращение должно ожидать своей очереди.
Можно выделить несколько методов диспетчеризации обращений:
−
Автоматическая диспетчеризация на основе очереди. Данный метод
позволяет сгладить пики нагрузки на систему, но имеет недостаток, присущий
всем очередям, при неправильной реализации время ожидания в очереди может
быть бесконечным.
−
Автоматическая диспетчеризация по событию. В данном случае
диспетчеризация обращений выполняется сразу же с поступлением данного
обращения в систему. Данный метод может работать быстрее чем решение с
очередью, но является менее надежным методом, так как при отсутствии
сглаживания пиков, система может перестать работать.
−
Ручная диспетчеризация обращений. Данный метод является наиболее
точным и безопасным, но при этом самым медленным, а также требующим
отдельного сотрудника.
−
Можно выделить несколько алгоритмов диспетчеризации обращений:
−
Честная диспетчеризация обращений. В таком случае, каждое
обращение имеет одинаковый приоритет относительно других обращений.
−
Диспетчеризация обращений с приоритетами. В данном методе
диспетчеризации некоторые обращения имеют более высокий приоритет,
следовательно, диспетчеризация таких обращений должна производиться в
первую очередь.
1.3 Единый маршрут сообщений для каждого канала связи
Для обеспечения гибкости системы и возможности ее индивидуальной
настройки для каждого заказчика, необходимо применять гибкую маршрутизацию
внутри самой системы. Требуемая маршрутизация должна позволять направлять
сообщения как на очередь операторов, так и на автоматических ответчиков,
интеграции со сторонними системами, сервисы рассылок по электронной почте,
ERP и CRM системы.
13
Для обеспечения настоящей омниканальности, маршрут сообщений должен
быть единым для всех каналов. В том числе, это возможно благодаря единому
формату
сообщений
внутри
системы.
Омниканальные
системы
могут
использовать подход, называемый конвейерной обработкой данных или data
pipelining.
Конвейер, или вычислительный конвейер – это множество единиц
обработки данных, соединенных последовательно, где выходной интерфейс одной
единицы – это точка входа для следующей. Элементы конвейера часто
исполняются одновременно или конкурентно.
Конвейеры бывают следующих видов:
1)
Вычислительные конвейеры, используемые в RISC архитектурах
процессоров. Такие конвейеры применяются в современных процессорах и
контроллерах для повышения их производительности.
2)
Графические конвейеры, наиболее часто используемые в большинстве
графических
ускорителей.
Такие
конвейеры
состоят
из
множества
арифметических единиц, или целых процессоров, реализующих различные этапы
распространенных операций растеризации (проекция перспективы, отсечение
невидимых граней, расчет света и цвета, растеризация).
3)
Конвейеры
программ,
состоящие
из
последовательности
вычислительных процессов (команд, запусков программ, задач, потоков,
процедур и т.д.), выполняемых параллельно, где выходной поток одного процесса
автоматически подается на вход следующему. Конвейер Unix-систем – это
классический пример этой идеи.
4)
Конвейерная обработка HTTP – технология, позволяющая выполнять
множество HTTP запросов, с использованием одного и то же TCP –подключения,
с возможностью начать новый запрос, не ожидая завершения предыдущего.
Применительно к омниканальным системам, конвейером могут служить
множество программных единиц обработки сообщений, и учитывая наличие
единой очереди в омниканальных системах, такого рода подход способен дать
высокую производительность и хорошую масштабируемость. Каждый отдельный
14
элемент конвейера всегда можно вынести на отдельный сервер, а интеракцию
между ними обеспечить по протоколу HTTP.
1.4 Обзор аналогов
Существует несколько аналогов разрабатываемой системы, которые имеют
свои преимущества, а также свои недостатки. Некоторые из аналогов
рассмотрены ниже.
1.4.1 Oktell Omnichannel
Oktell – это коммуникационная платформа для автоматизации контактцентров крупного и среднего бизнеса. Является высокотехнологичным решением,
обеспечивающим интеграцию в бизнес-процессы, сквозной контроль качества
обслуживания и оптимизацию затрат на организацию клиентского сервиса [5].
Oktell позволяет объединить неограниченное количество
голосовых
(телефонные каналы, WebRTC, Skype) и неголосовых каналов связи (e-mail,
мессенджеры, веб-чаты) в одну систему. Благодаря этому создается единая точка
контроля всех коммуникаций и появляется возможность оперативно влиять на
процессы обслуживания
Чат-центр объединяет веб-чаты, электронную почту, социальные сети,
мессенджеры и мобильные приложения в единую интеллектуальную систему. За
счет широких интеграционных возможностей решение легко подключается к
существующей телефонии и технологической инфраструктуре компании.
Оптимизировать обработку сообщений в цифровых каналах помогает
текстовое меню, которое является аналогом IVR в голосовых каналах. Эта
технология
позволяет
определять
тематику
текстовых
обращений
и
в
автоматическом режиме отвечать на легкие и стандартные вопросы. Для
реализации данной технологии система обращается к корпоративной базе знаний.
Сложные и нестандартные вопросы переводятся на операторов, а по итогам
обработки контакта система предлагает клиенту оценить качество обслуживания.
15
Чат-центр позволяет обеспечить единые стандарты обработки клиентских
обращений.
Независимо
от
того, какой
текстовый
канал используется,
обслуживание клиентов будет осуществляться по установленному сценарию.
Ответственные сотрудники в любое время могут проверить, насколько корректно
соблюдаются корпоративные стандарты коммуникации.
Платформа Oktell позволяет освободить операторов от выбора, на какое
обращение отвечать в первую очередь – можно задать алгоритмы обслуживания
клиентов и разрешить сотрудникам одновременно обрабатывать только одно
обращение, предложенное системой. Это позволяет повысить эффективность
контакт-центра и сократить количество случаев, когда оператор превышает время
ожидания или вовсе забывает ответить абоненту.
Технологии Oktell позволяют осуществлять маршрутизацию текстовых
обращений
в
автоматическом
режиме.
Посредством
анализа
истории
коммуникаций с клиентом решение распознает тематику запроса и направляет его
наиболее релевантному сотруднику. Для реализации данной функции требуется
интеграция с CRM-системой и телефонией компании.
Предусмотрено получение актуальной информации о скорости ответа,
текущей нагрузке и производительности операторов. Чат-центр позволяет
контролировать процесс обработки всех видов обращений в режиме реального
времени. Система мгновенно оповещает супервизора о любом отклонении от
нормативных значений. Есть возможность осуществления массовых e-mail
рассылок, отправления сообщения через мессенджеры по установленному
сценарию.
Чат-центр
позволяет
реализовывать
исходящие
текстовые
коммуникации и задавать алгоритм обработки ответов. При этом могут быть
задействованы как боты, так и операторы.
Чат-центр
позволяет
использовать
любые
цифровые
каналы.
При
подключении новых каналов коммуникации отпадает необходимость каждый раз
настраивать алгоритмы их обработки – система сделает это автоматически по
ранее заданному сценарию.
16
1.4.2 Омниканальная платформа онлайн-консультирования LiveTex
На
данный
момент
LiveTex
[6]
—
самая
крупная
компания,
предоставляющая мультисервисную платформу, которая, несомненно, полезна
среднему и крупному бизнесу. Эта платформа объединяет сайт, мессенджеры,
группы в социальных сетях, CRM и чат-боты в одно омниканальное решение
(рисунок 1.3).
Рисунок 1.3 – Интерфейс омниканальной платформы LiveTex
LiveTex предлагает:
−
одно рабочее приложение для сотрудников вместо 5-6 разных окон;
−
подключение чат-ботов вместо первой линии поддержки;
−
получение карточки клиента из CRM без переключения окон и
вкладок;
−
использование
нейросетей
для
обучения
и
повышения
компетентности операторов.
Онлайн-сервисы LiveTex могут быть интегрированы с внутренними
информационными системами компаний, такими как CRM. Интеграция позволяет
просматривать историю обращений и сведения о клиенте во время чата; создавать
правила для вовлечения в диалог на основе истории посетителя, поискового
17
запроса, просматриваемой страницы, времени нахождения на сайте — и делать
индивидуальные предложения каждому клиенту.
К достоинствам платформы следует отнести:
1)
Возможность формирования подробных аналитических отчетов по
работе менеджеров с клиентами – анализируются любые показатели, начиная с
рабочего времени оператора и заканчивая тем, сколько сообщений приходит по
каждому каналу.
2)
Настройка всевозможных фильтров для формирования нужных
отчетов за любой период и с любыми показателями.
3)
Удобство работы с историей диалогов – она отображается по каждому
клиенту в виде цепочек, ее удобно читать и делать выводы о том, с каким
вопросом пришел клиент и как его обслужили.
4)
Возможность настройки сценариев и отображение статистики по ним
(сколько раз сценарий был показан за день – это дает возможность
сориентироваться, работают сценарии вообще или нет).
5)
Способность платформы формировать целостную карточку клиента
по контактным данным (например, клиент обращается из социальной сети, указав
в тексте свой номер телефона, и система автоматически показывает менеджеру
его предыдущую переписку из чата, почты, даже заказов звонка).
В качестве недостатков можно указать не реализованную возможность
перехода из чата, например, в другой канал при переписке, а также высокую
стоимость платформы, что неприемлемо для небольших компаний.
1.4.3 RedHelper
RedHelper – это онлайн-консультант [7] с большими функциональными
возможностями,
большим
достоинством
которого
является
«совместный
браузер». С его помощью можно наблюдать за действиями пользователя
непосредственно в окне приложения оператора, переводить на необходимую
страницу или подсвечивать элементы. Используя «совместный» браузер, можно
приглашать в чат именно тогда, когда пользователю это нужно (легко заметить по
18
его действиям на сайте) и сразу же предложить решение того вопроса, что его
заинтересовал (рисунок 1.4).
Рисунок 1.4 – Онлайн-консультант RedHelper
К другим достоинствам можно отнести возможность отправить диалог сразу
на электронную почту, а также наличие статуса «вторая линия». Оператор с этим
статусом не принимает новые диалоги от пользователей, но другие операторы все
же могут перевести беседу – эта функция подойдет для руководителя отдела,
который только изредка общается в чате, в особо сложных случаях.
Настройка сценариев «Активных продаж» позволяют таргетировать
появление приглашения пользователя в чат – в этом и состоит залог высокой
конверсии: дать пользователю помощь только тогда, когда ему это необходимо.
Недостатками RedHelper можно назвать нехватка управленческих и
контрольных функций, которые могут оказаться недостаточными для сегмента
крупного и особо крупного бизнеса. 5-10 операторов онлайн – идеальный
максмимум
для
RedHelper.
Большее
количество
будет
уже
трудно
контролировать, особенно если операторы сидят в разных местах – отсутствует
межоператорский чат и функции руководителя операторов, как это реализовано в
Livetex.
С другой стороны, RedHelper предлагает красочные и понятные отчеты, как
по сайту в целом, так и по каждому оператору в отдельности. Особенность данной
19
системы заключается в том, что RedHelper автоматически сравнивает действия
всех операторов системы в «облаке», и показывает эффективность их работы в
режиме реального времени, при этом оценка происходит по совокупности
параметров.
1.4.4 Jivosite
Jivosite – cамый распространенный на рынке консультант, одним из
достоинств которого является наличие бесплатной версии [8].
Из плюсов данной системы – удобный личный кабинет, простое и быстрое
подключение всех нужных каналов, удобное приложение оператора, где,
накапливается история ответов менеджера и формируется список его личных
быстрых ответов, чтобы была возможность быстрее отвечать на типовые вопросы
(рисунок 1.5).
К отрицательным моментам можно отнести следующее:
−
отчеты по каждому диалогу приходят на почту;
−
неудобная форма создания статистических отчетов;
−
малый срок хранения истории общения с клиентами (60 дней).
Рисунок 1.5 – Интерфейс онлайн-консультанта Jivosite
20
1.4.5 Naumen Оmni-Сhannel
Naumen Оmni-Сhannel – омниканальная платформа [9], которая позволяет
компании обеспечить эффективную коммуникацию с клиентами по следующим
каналам связи (рисунок 1.6):
−
телефонные вызовы, в том числе: звонок с сайта (WebRTC), запрос на
звонок с сайта;
−
мобильные приложения;
−
электронная почта
−
SMS;
−
веб-чат;
−
мессенджеры (Facebook, Telegram, ВКонтакте);
−
социальные медиа (Facebook, ВКонтакте, Twitter).
Рисунок 1.6 – Интерфейс омниканальной платформы Naumen Оmni-Сhannel
В Naumen Оmni-Сhannel реализованы основополагающие принципы
омниканального
обслуживания:
клиенты
получают
одинаковый
и
непротиворечивый сервис, независимо от того, по какому каналу они обратились.
Это обеспечивается с помощью таких инструментов, как:
21
−
единая очередь обработки обращений (общий ACD);
−
единые правила маршрутизации;
−
предобработка обращения из любого канала;
−
единый пул операторов;
−
единые операторские сценарии обработки обращений;
−
идентификация клиента при обращении по любому каналу;
−
единая история взаимодействия с клиентом по всем каналам omni-
channel;
−
единая база знаний с подсказками по типовым вопросам;
−
возможность перевода обращения на другую очередь;
−
возможность кросс-канального взаимодействия;
−
блендинг операторов между каналами (оператор может получить
следующее сообщение из любого канала);
−
возможность одновременного обслуживания нескольких сессий;
−
гибкие политики распределения мгновенных сообщений с учетом
разной скорости их обработки отдельными операторами.
Для оптимизации процесса обслуживания Naumen Оmni-Сhannel позволяет
настраивать сценарии для любых типов кампаний:
−
возможность использования единого сценария для разных каналов;
−
возможность использовать в сценарии любую информацию из
сторонних систем;
−
категоризация тем обращений по всем каналам;
−
отображение в сценарии всей истории взаимодействия с клиентом.
Для анализа эффективности и качества обслуживания в Naumen ОmniСhannel реализована встроенная система отчетности, отражающая требования
международных стандартов EN15838 и COPC:
−
сводные отчеты для анализа отдельных каналов в разрезе операторов,
проектов, причин обращения, качества обслуживания и т.д.;
22
−
кастомизация
интерфейса,
отчетности,
сценариев
обработки
контактов и др. Предусмотрена интеграция с любыми внешними системами (АБС,
CRM, Billing, HelpDesk, и т.п.).
Сравнение аналогов представлено в таблице 1.
Таблица 1 – Сравнение аналогов
Функционал
Oktell
LiveTex
RedHelper
Jivosite
1
Возможность перевода
обращения другому оператору
Возможность интеграций
Возможность легкого
масштабирования компонентов
Гибкая настройка маршрутов в
системе
Поддержка множества
алгоритмов диспетчеризации
Наличие межканального
взаимодействия
Поддержка чат-ботов
Автоматическая диспетчеризация
обращений
Построение статистики в
реальном времени
Мониторинг работы операторов
Гибкое построение отчетов
2
3
4
5
Naumen
Omnichann
el
6
+
+
-
-
+
+
+
-
+
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
-
-
+
+
+
+
-
+
+
+
+
-
+
+
-
+
+
+
-
+
-
+
+
Оценив недостатки и достоинства аналогов, составим требования для
разрабатываемой системы:
1)
Система должна обладать возможностью интеграций со сторонними
ERP и CRM системами. Интеграции открывает широкие возможности по
привлечению
новых
заказчиков
и
добавлению
нового
функционала
с
минимальным количеством доработок.
2)
Система
должна
обладать
возможностью
горизонтального
масштабирования компонентов для устойчивости к любой нагрузке. Таким
образом, система сможет справляться с нагрузкой мелких и крупных заказчиков.
23
3)
Обязательным требованием к разрабатываемой системе является
возможность перевода обращений между операторами. Это поможет направить
обратившегося в поддержку клиента к более квалифицированному оператору.
4)
Для
обеспечения
омниканальности
необходим
функционал
межканального взаимодействия. Обратившись в один канал, пользователь должен
иметь возможность перейти в другой, при этом обращение должен вести один и
тот же оператор.
5)
Поддержка чат-ботов также является обязательным требованием, это
поможет снизить нагрузку с операторов и освободить их от ответов на самые
распространенные вопросов.
6)
Система
должна
обладать
механизмом
автоматической
диспетчеризации обращений с помощью единой операторской очереди. Данное
решение позволит снизить пики нагрузки.
7)
Алгоритм диспетчеризации должен легко поддаваться изменению и
замене.
8)
Разрабатываемая система должна иметь возможность построения
подробной статистики и отчетов.
9)
Система должна быть отзывчивой и обеспечивать минимальные
задержки в доставке сообщений и диспетчеризации обращений.
24
2 КОНЦЕПТУАЛЬНОЕ ПРОЕКТИРОВАНИЕ ПРОГРАММНОЙ
СИСТЕМЫ ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ
РАЗНОРОДНЫХ СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
2.1 Разработка функциональных спецификаций
Для разработки функциональных спецификаций необходимо составить
модель предметной области. Для успешного решения данной задачи подходит
диаграмма вариантов использования [10]. Данная диаграмма изображена на
рисунке 2.1.
Рисунок 2.1 – Диаграмма вариантов использования
На диаграмме изображено четыре актора — основные роли, пользователей
или систем, которые производят интеракции с разрабатываемой системой.
Первый актор — это агент или оператор. Под агентом понимается любая
сущность, способная отправлять ответы в обращения клиенту. Таким образом,
агентами являются сервис автоответов, сервис опросов, сервис интеллектуальных
чат-ботов, сервис интеграции рабочего места операторов. Первоочередной
задачей агентов является ведение обращения с клиентом.
25
В ведение обращения входит отправка текстовых и графических сообщений
клиентам, установка тематики обращения, управление жизненным циклом
обращения, завершения обращения.
Предполагается что база знаний используется в основном оператором для
получения актуальной информации по продуктам заказчика, однако базой знаний
могут пользоваться и автоматические агенты, реализующие автоматическое
ведение пользователя по сценарию, или интеллектуальные чат-боты.
Актор клиент имеет возможность общаться с техподдержкой и разрешать
свои проблемы в любом удобном ему текстовом канале, в котором представлен
бренд заказчика.
Актор менеджер имеет возможность оценивать работу операторов
посредством мониторинга текущих обращений, просмотра статуса операторов в
реальном времени. Мониторинг текущих обращений позволяет оценивать
нагрузку на контактный центр и предпринимать действия по подключению
дополнительных операторов, или отключению операторов.
Вариант использования «просматривать историю обращений» позволяет
менеджеру оценивать работу операторов в ретроспективе, на основе чего
менеджер может скорректировать рабочий процесс операторов. Просмотр
истории обращений можно отнести к анализу работы контакт-центра в близком
приближении.
Просмотр статистики является неким взглядом «сверху» на работу контактцентра. Для менеджера важно видеть показатели эффективности операторов,
общую удовлетворенность клиентов, средние оценки работы операторов
клиентами, время работы операторов, время ожидания обращений в очереди и т.д.
Данные показатели также могут использоваться для построения отчетов
заказчику. Менеджер также имеет возможность заполнять корпоративную базу
знаний, используемой операторами и другими агентами при ведении диалога.
Последним актором, взаимодействующим с разрабатываемой системой
является актор «сторонние системы». Под данным актором подразумеваются
26
любые сторонние системы, с которыми в будущем возможно взаимодействие.
Сюда входят ERP и CRM-системы, багтрекеры, корпоративные системы и т.д.
Не всегда возможна своевременная реализация выгрузки отчета в уже
имеющейся системе, в таком случае необходимо чтобы разрабатываемая система
имела возможность выгрузить «сырые» данные в виде истории обращений со всей
метаинформацией
об
обращениях.
Таким
образом,
можно
переложить
ответственность за построение отчетов и ведение статистики, требуемой
заказчику, на стороннюю систему. Данный вариант использования обозначен на
диаграмме как «выгружать историю обращений».
Аналогично, иногда имеющаяся в системе статистика может быть слишком
детальной, требуется провести различные операции над статистическими
данными, либо отобразить их несколько иначе, в таком случае может быть
полезна выгрузка статистики из разрабатываемой системы.
Для интеграций немаловажной функцией является возможность отправки
сообщений напрямую пользователям. С помощью данной функции выполняется
целый спектр задач: от организации массовых рассылок, до персонализированной
таргетированной рекламы.
2.2 Общая структура разрабатываемой системы
Определившись с функциональными требованиями, необходимо составить
общую структуру разрабатываемой системы, выделить внешние по отношению к
системе сущности, а также выявить взаимосвязь и потоки данных между ними.
Для обозначения самого верхнего уровня системы было разработано
схематичное представление потоков данных между системой и внешними
объектами,
представленное
на
рисунке
2.2.
Специфицируемая
система
представляется в виде одного единственного объекта, связанного с внешними
сущностями потоками данных. Данная диаграмма представляет требования к
системе на самом верхнем уровне - уровне взаимодействия с окружением
27
Оператор
Ведение
диалога
Сторонние системы
Авторизация пользователей
Синхронизация статусов операторов
Информация о клиентах
Сценарии операторов
База знаний
Обращения
Входящие сообщения
Система
(ERP, CRM, HelpDesk и др.)
Отчетная
информация
История
обращений
Информация о
пользователях
Менеджер
Исходящие сообщения
Каналы
(мессенджеры,
E-Mail, SMS)
Сообщения Тематика
клиентов
Намерения
Классификаторы
Рисунок 2.2 – Потоки данных между системой и внешними объектами
Центральным объектом схемы является разрабатываемая система, её
окружают внешние относительно неё объекты – сторонние системы и сущности,
стрелками изображены потоки данных.
Первым внешним объектом, относительно системы, являются каналы. Под
каналами
понимаются
любые
средства
текстового
общения,
такие
как
мессенджеры, E-Mail, SMS и тому подобные. В данных каналах основными
данными являются текстовые сообщения, а также события, происходящие как в
разрабатываемой системе, так и в канале. Примеры таких событий:
−
оператор набирает сообщение;
−
клиент набирает сообщение;
−
сообщение было прочитано;
−
сообщение было доставлено.
Под входящими сообщениями понимаются сообщения, приходящие из
канала в систему. Исходящие сообщения – сообщения, уходящие в канал.
Существует
специфика
преобразования
входящих
сообщений
во
внутрисистемные, и внутрисистемных сообщений в исходящие. Данный вопрос
будет решаться с помощью специального интерфейсного модуля.
Блок сторонних систем на контекстной диаграмме соответствует актору
интеграций на диаграмме вариантов использования. Исходящими потоками
данных из системы являются:
28
1)
Отчетная информация. Сторонние системы могут выгружать отчеты в
указанном формате с помощью специального интеграционного интерфейса.
2)
История обращений. Может быть выгружена из системы также с
помощью специального интеграционного интерфейса.
3)
Информация о пользователях.
Входящие потоки данных в разрабатываемую систему:
1)
Аутентификационная информация клиентов. В некоторых случаях
необходимо реализовывать аутентификацию клиентов в каналах через стороннюю
систему, уже имеющую единую базу данных клиентов. Такая аутентификация
необходима для плотной интеграции с корпоративными CRM и ERP-системами.
2)
Синхронизация статусов операторов. Существуют случаи, когда
операторы работают параллельно в нескольких системах, в таких случаях
необходимо производить синхронизацию статусов операторов в обоих системах.
3)
Сценарии операторов. Операторам удобно иметь всю необходимую
для работы информацию в едином окне. Для этого необходимо отображать
сценарии работы прямо внутри системы.
4)
База знаний. Порой у заказчика уже имеется корпоративная база
знаний, и отказ от нее не возможен, в таком случае необходимо синхронизировать
базу знаний в разрабатываемой системе с уже имеющейся базой.
Классификаторы
разрабатываемой
также
системы.
являются
Под
внешним
классификаторами
объектом
относительно
понимаются
системы,
осуществляющие операции классификации входящих сообщений от клиентов по
намерениям и тематикам, определенным в базе знаний.
Входящими потоками данных в классификаторы являются сообщения
клиентов. Исходящими потоками данных из классификаторов являются тематики
и намерения.
Четвертым внешним объектом относительно системы являются операторы.
Операторы производят управление жизненным циклом диалога, используют базу
знаний для получения актуальной информации. Также под операторами можно
29
понимать менеджеров, так как менеджеры являются обобщением сущности
операторов.
Потоками данных между операторами и системой можно считать
статистическую
информацию,
текущие
обращения,
историю
обращений,
информацию о клиентах.
Для того, чтобы составить представление о системе и её границах внутри
всей экосистемы объектов, с которыми она связана, была построена карта
экосистемы [11].
Карта экосистемы (ecosystem map) показывает все системы, связанные с
создаваемой системой и взаимодействующие друг с другом, а также природу этих
взаимодействий. Карта экосистемы представляет рамки путем отображения всех
систем, которые взаимосвязаны друг с другом и которые может потребоваться
изменить при создании системы. Карта экосистемы отличается от контекстных
диаграмм тем, что показывает другие системы, связанные с разрабатываемой, в
том числе без непосредственных интерфейсов. Зависимые системы можно
определить путем выявления тех, что потребляют данные, поступающие из вашей
системы. Достигнув точки, в которой разрабатываемый проект не влияет ни на
какие дополнительные данные, можно сказать, что достигнута граница систем,
которые относятся к решению.
Построенная карта экосистемы изображена на рисунке 2.3.
Корпоративные
базы данных
Оператор
Сторонние системы
Сообщения
Взаимодействие
Система
Заказчик
CRM, ERP
HelpDesk, багтрекеры
Менеджер
Сообщения
Сообщения
Тематики
Каналы
Сообщения
Клиенты
Классификаторы
Рисунок 2.3 – Карта экосистемы
30
Клиенты обращаются в систему через каналы, в которых имеются
представительства заказчика. Единственным видом данных между клиентами и
каналами являются сообщения.
Менеджеры также имеют прямой доступ к системе. Основной задачей
менеджеров в системе является учет статистики, контроль ведения обращений
операторами, контроль режима работы операторов, а также проверка истории
обращений.
Операторы, менеджеры и система связаны с целой группой систем, уже
имеющихся у заказчика. Операторы используют системы заказчика для
получения актуальной справочной информации для ведения обращения.
Менеджеры могут использовать системы для учета, ведения баз данных и баз
знаний. Разрабатываемая система же напрямую интегрируется с системами
заказчика для обеспечения более широкого функционала.
Заказчик не имеет непосредственного отношения к системе, все действия в
интересах заказчика выполняют операторы или менеджеры. Заказчик и его
подразделения ответственны в основном за наполнение и ведение внешних
систем.
Для того чтобы составить полное представление о системе, необходимо
составить список внешних событий.
Внешними событиями системы являются:
−
поступило входящее сообщение из канала;
−
оператор сменил статус в системе;
−
оператор отправил сообщение;
−
оператор перевёл обращение на другого оператора;
−
оператор завершил обращение;
−
оператор установил тематику обращения.
Необходимо выделить подсистемы в разрабатываемой системе. Данными
подсистемами являются:
−
подсистема маршрутизации сообщений;
−
интерфейсная подсистема для интеграции каналов;
31
−
подсистема диспетчеризации сообщений;
−
подсистема для сторонних интеграций.
На рисунке 2.4 изображена общая архитектура разрабатываемой системы.
Операторы
Отправка сообщений
Ведение обращений
ПМ: Очередь сообщений,
Активные обращения,
Активные пользователи
ПД: Очередь обращений,
Операторы и связанная
информация
Отправка сообщений
Отправка оповещений
Подсистема
Диспетчеризации
(ПД)
Отправка
сообщений
Опер.
БД
К1
Запрос обращений,
пользователей
и т.д.
К2
Подсистема
интеграции
каналов
...
Клиенты
Подсистема
Маршрутизации
(ПМ)
КN
Каналы
Архивн.
БД
История обращений,
архивные пользователи,
архивные сообщения
и т.д.
Рисунок 2.4 – Общая архитектура разрабатываемой системы
2.3 Информационная модель системы
Составив общий план системы, необходимо разработать информационную
модель системы. Добиться этого можно формализовав потоки данных, входящие
и исходящие из системы, а также протекающие внутри системы.
Центральная сущность системы – это сообщения. На рисунке 2.5
представлена общая схема движения входящего сообщения из канала через
систему.
Каналы
1
Интеграционный
Интерфейс
3
2
Подсистема
маршрутизации
Подсистема
диспетчеризации
4
Оператор
Рисунок 2.5 – Общая схема движения входящего сообщения через систему
32
Сообщение, поступив от клиента в канал, следом поступает в специальный
интерфейс, осуществляющий интеграцию данного канала с системой. Интерфейс
производит первичную обработку сообщения и преобразование сообщения в
формате канала во внутрисистемный формат. Затем сообщение поступает в
общую очередь обработки. Следом подсистема маршрутизации обрабатывает
сообщение из очереди. Обработав сообщение, подсистема маршрутизации
передаёт
сообщение
далее,
в
подсистему
диспетчеризации.
Подсистема
диспетчеризации выбирает свободного оператора, затем отправляет сообщение
данному оператору.
На схеме числами выделены следующие потоки данных:
1)
Входящий поток сообщений из каналов в интеграционный интерфейс.
Сообщения представляются в формате канала. Здесь предъявляются высокие
требования к отказоустойчивости и пропускной способности интеграционного
интерфейса, ведь это сильно влияет на надёжность всей системы. Попав в
интеграционный интерфейс, сообщение преобразуется во внутрисистемный
формат. Возможны дополнительные запросы к каналу для специфичных для
канала задач.
2)
Поток
внутрисистемных
сообщений
между
интеграционным
интерфейсом и подсистемой маршрутизации. Предъявляются требования к
отказоустойчивости и пропускной способности модуля постановки в единую
очередь сообщений. Чтобы обеспечить сглаживание пиков нагрузки, используется
принцип producer-consumer [12].
3)
Поток
внутрисистемных
сообщений
между
подсистемой
маршрутизации и подсистемой диспетчеризации. Разборщик очереди, получив
очередное сообщение, начинает его обработку. Здесь происходит создание
обращения, его обновление, запись статистических данных в базу данных,
создание клиента в системе и т.д. К подсистеме диспетчеризации также
предъявляются требования к отказоустойчивости и высокой пропускной
способности.
33
4)
Поток данных между подсистемой диспетчеризации и операторами. В
данном случае скорость обслуживания клиентов напрямую зависит от количества
операторов, одновременно использующих систему.
Была составлена диаграмма коммуникации, показывающая взаимодействие
компонентов системы при обработке входящего сообщения. Данная диаграмма
представлена на рисунке 2.6.
Рисунок 2.6 – Диаграмма коммуникации обработки входящих сообщений
Интерфейс интеграции каналов является довольно простым. Основной
задачей данного интерфейса является приём сообщений из каналов с помощью
реализации
интерфейсов,
требуемых
интегрируемым
каналом,
либо
прослушивание канала, например, методом long polling [12].
Для реализации принципа единого маршрута для каждого сообщения по
принципу конвейера, необходима некая сущность, представляющая единицу
обработки конвейера. Обработка сообщений в конвейерном режиме возложена на
подсистему маршрутизации.
Агент – это любая программная сущность, способная принимать сообщения
и отвечать на них. Агенты должны быть легко конфигурируемы для любого
заказчика,
должны
масштабированию,
а
обладать
также
быть
возможностью
к
взаимозаменяемыми.
горизонтальному
Концептуально
34
взаимодействие агента и подсистемы маршрутизации можно представить в виде
схемы, изображенной на рисунке 2.7.
Агент
Подсистема
маршрутизации
т)
(отве
е
и
н
ще
сооб
е
е
щ
дя
Исхо
Входящ
ее
сообщ
ение
Бизнес-логика агента
Интеграционный интерфейс
агента
Рисунок 2.7 – Взаимодействие агента и подсистемы маршрутизации
Используя концепцию агентов, мы можем реализовать интеграцию
подсистемы маршрутизации и подсистемы диспетчеризации с помощью
реализации интеграционного интерфейса агента в подсистеме диспетчеризации.
Таким же образом, классификаторы можно представить в виде агентов,
реализовав лишь интерфейсную часть агента. Агент имеет возможность ответить
на сообщение как в синхронном режиме, так и в асинхронном.
Концепция
агентов
позволяет
иметь
множество
самостоятельных
подсистем, объединенных в одно целое единым интерфейсом, что даёт
следующие преимущества:
−
легкость горизонтального масштабирования: каждый агент может
являться как самостоятельным процессом, так и модулем другого процесса;
−
высокая отказоустойчивость, ведь отказ одного агента не влечёт за
собой отказ всей системы;
−
легкость внесения изменений: при изменении бизнес-логики клиента
достаточно изменить маршрут сообщения с помощью замены, изменения или
исключения агента;
−
заказчика;
возможность конфигурации индивидуального маршрута для каждого
35
−
возможность многоразового переиспользования уже имеющихся
маршрутов.
Агенты
объединяются
в
единое
целое
с
помощью
подсистемы
маршрутизации. Сообщение, попав в подсистему маршрутизации, передаётся
первому агенту в цепочке. Ответ агента снова возвращается в данную подсистему,
затем пересылается следующему агенту и так далее. Схематично данное
взаимодействие представлено на рисунке 2.8.
Подсистема
маршрутизации
1
2
Агент 1
3
4
Агент 2
...
2*N – 1
2*N
Агент N
Рисунок 2.8 – Взаимодействие агентов и подсистемы маршрутизации
Выполнение данной цепочки может прекратиться в тот момент, когда у
агента есть ответ на входящее сообщение. Это можно реализовать с помощью
типов сообщений. Например, одни агенты, могут отвечать только на сообщения,
обозначающие начало диалога с клиентом (например, /start в канале Telegram
[13]), другие могут отвечать только на сообщения, представленные в
определенном формате. Можно реализовать агента, отвечающего специально
подготовленным текстом только в ночное время, либо агента-автоответчик при
недоступности операторов.
Задачей подсистемы диспетчеризации является непосредственно назначение
обращений
на
операторов.
Операторы
должны
иметь
возможность
регистрироваться в данной системе, и изменять свой статус. Возможные статусы
операторов:
36
−
в сети – оператор принимает новые обращения и ведёт работу над уже
имеющимися;
−
не в сети – оператор не активен в данной подсистеме;
−
невидимый
–
оператор
завершает
имеющиеся
обращения,
и
недоступен для приёма новых обращений.
Также
можно
выделить
некоторые
особые
состояния
операторов,
характеризующие их занятость:
−
оператор на обращении;
−
оператор в ожидании;
−
оператор не в сети.
Необходимо вести учет каждого из этих состояний для возможности
построения отчетов.
2.4 Контекстная диаграмма классов
Теперь,
когда
функциональные
и
информационные
спецификации
составлены, мы можем объединить все сущности в единой контекстной
диаграмме классов [10]. На рисунке 2.9 изображена разработанная контекстная
диаграмма классов.
Рисунок 2.9 – Контекстная диаграмма классов
37
Обращение – это тип данных, представляющий собой единое обращение
клиента в контакт-центр. Учитывая омниканальный характер системы, обращение
может начаться в одном канале, а продолжится в другом. Поэтому, обращение
имеет связь один ко многим с сущностью «сессия». Во время работы над
обращением, оператор может выйти из системы, произойти отключение, либо
оператор сочтет нужным перевести обращение, поэтому на контекстной
диаграмме классов также отображена связь один ко многим между обращением и
операторами. Также возможен случай, что ни один из операторов в принципе
никогда не получит обращение. Это возможно в том случае, если контакт-центр
обслуживают интеллектуальные чат-боты. Сущность обращение также будет
накапливать статистику клиента по текущему обращению. Каждое обращение
принадлежит лишь одному заказчику.
Сущность сообщение представляет собой текстовое или сообщение с
медиаконтентом, поступившее в систему. На данной контекстной диаграмме не
отображены сообщения, поступающие из каналов, так как всегда происходит
конвертация сообщений внешних форматов в сообщения внутрисистемного
формата. Типы контента сообщений бывают следующих видов:
−
текст;
−
изображение;
−
кнопки (действия, предлагаемые клиенту);
−
геопозиция.
Каждое сообщение принадлежит только одной сессии. Также сообщения
имеют связь с сущностью агентов для отслеживания агентов, ответственных за
тот или иной ответ.
Сессия – сущность, ответственная за учет идентификационных и
статистических данных клиента и оператора. Граница ответственности сессии
начинается с обращения клиента в канал и заканчивается с переходом клиента в
другой канал, либо с завершением диалога, либо со сменой оператора. Данная
сущность необходима для реализации всех требований к омниканальной системе.
С помощью данной сущности мы имеем возможность собирать статистические
38
данные по каждому каналу и по каждому оператору, что позволит строить любые
необходимые для заказчика отчеты. Также данная сущность используется для
отслеживания в каком из каналов в данный момент происходит взаимодействие
клиента и системы. В рамках каждой сессии возможен лишь один агент, лишь
один канал и лишь один оператор. Сессии содержат в себе множество сообщений.
Сущность заказчик представляет собой идентификатор заказчика и служит
для разделения обращений и прочих сущностей, а также для последующей
фильтрации
статистики
по
заказчику.
Один
экземпляр
системы
может
обслуживать одновременно несколько заказчиков, что позволяет рационально
использовать ресурсы вычислительной системы.
Сущность конфигурация заказчика представляет собой совокупность
конфигураций для различных подсистем. Так, сюда включается конфигурация
маршрута агентов, конфигурация множества самих агентов, конфигурация
каналов и так далее.
39
3 ДЕТАЛИЗИРОВАННОЕ ПРОЕКТИРОВАНИЕ ПРОГРАММНОЙ
СИСТЕМЫ ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ
РАЗНОРОДНЫХ СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
3.1 Детализированная диаграмма классов
Перед
проектированием
подсистем,
составим
диаграмму
Разработанная диаграмма классов представлена на рисунке 3.1.
Рисунок 3.1 – Детализированная диаграмма классов
классов.
40
Приведем таблицу соответствий сущностей контекстной диаграммы классов
и латинских названий на детализированной диаграмме классов:
Таблица 3.1 – Соответствие названий сущностей
Сущность
Латинское название
Обращение
Dialog
Клиент
User
Сообщение
Message
Сессия
Session
Оператор
Operator
Заказчик
Customer
Агент
Agent
Компетенции оператора
Routing
Сущность «конфигурация заказчика» была объединена с сущностью
«заказчик», так как данный вариант проще для реализации. Были разработаны
вспомогательные классы, реализующие связи многие ко многим, а также
позволяющие достигнуть меньшей связности в коде. К таким сущностям
относятся:
−
Stat – сущность, представляющая собой статистическую информацию
о диалоге или сессии;
−
Pair – сущность, позволяющая более гибко конфигурировать каналы и
агентов;
−
UserChannel – сущность, реализующая связь многие ко многим между
клиентом и каналом.
Были выделены дополнительные перечислимые типы:
−
MessageType – типы сообщений;
−
OperatorStatus – возможные статусы операторов;
−
ChannelType – типы каналов.
41
Рассмотрим каждый из них подробнее. Перечислимый тип данных
MessageType представляет собой все возможные типы сообщений. Выделены
следующие типы сообщений:
−
Initial – сообщение, обозначающее начало диалога с системой,
например, /start в Telegram;
−
Message – обычное сообщение;
−
SentConfirmation – подтверждение отправки сообщения в канал;
−
DeliveredConfirmation – подтверждение доставки сообщения в канал;
−
ReadConfirmation – подтверждение о том, что сообщение было
прочитано;
−
Failed – отправка сообщения не удалась.
Тип OperatorState представляет собой все возможные статусы, в которых
может пребывать оператор, это статусы:
−
Offline – оператор не в сети;
−
Online – оператор в сети;
−
DoNotRoute – оператор в невидимом режиме.
На рисунке 3.2 приведены возможные переходы между статусами
оператора.
Не в сети
Невидимый
В сети
Рисунок 3.2 – Смена статусов операторов
Тип ChannelType представляет собой перечисление всех возможных видов
каналов, которые поддерживаются в системе.
42
3.2 Проектирование подсистемы интеграции каналов
Перейдем к проектированию подсистемы интеграции каналов. Основными
задачами данной подсистемы является интеграция и взаимодействие со всеми
возможными внешними текстовыми каналами. При разработке архитектуры
необходимо использовать модульный подход. Нужно обеспечить низкий уровень
самоповторений при реализации данной подсистемы.
Схема взаимодействия данной подсистемы с внешними объектами
изображена на рисунке 3.3.
Система
Отправка
исходящих
сообщений
Подсистема
маршрутизации
Отправка
входящих
сообщений
Интеграция 1
Отправка
сообщений
Канал 1
Интеграция 2
Канал 2
...
...
Интеграция N
Канал N
Подсистема интеграции
Опер.
БД
Рисунок 3.3 – Схема взаимодействия подсистемы интеграции каналов и внешних
объектов
Подсистема интеграции состоит из множества модулей интеграций,
реализующих интеграцию одного из существующих каналов. Данные модули
интеграции ответственны за конвертацию и отправку исходящих сообщений в
каналы, а также за прием и конвертацию во внутрисистемный формат входящих
сообщений.
Каждый из модулей интеграции имеет типовую структуру. Модуль
интеграции обязательно состоит из класса-интеграции и опционально из HTTP-
43
контроллера
(здесь
подразумевается
контроллер
в
модели
MVC
[12]),
отвечающего за интеграцию с системами, поддерживающими интеграцию по
HTTP.
В
данных
модулях
применен
объектно-ориентированный
подход
разработки. Класс интеграции можно условно назвать «каналом», однако во
избежание путаницы использования этого термина будем избегать.
Нужно отметить, что входной точкой в систему для клиентов являются
специальные «представители» заказчика в интегрируемом средстве обмена
сообщениями. Примерами таких «представителей» являются:
−
бот, созданный с помощью @botfather [14] в мессенджере Telegram;
−
публичная страница в социальных сетях VK и Facebook [15, 16];
−
номер телефона для канала SMS;
−
адрес электронной почты для канала E-Mail.
Хотя наиболее легкой интеграции поддаются каналы, использующие JSON
или XML-протоколы поверх HTTP, интеграция каналов, использующих другой
транспорт, или вообще не представленных в сети интернет, также возможна.
Например, интеграция канала E-Mail осуществляется посредством использования
POP3 клиента для прослушивания единого почтового ящика, используемого в
качестве единой точки входа. Таким образом, поступающие на данный почтовый
ящик сообщения являются входящими сообщениями относительно системы, а
почтовый адрес клиента, отправившего электронное сообщение на данный
почтовый ящик, является уникальным идентификатором клиента в данном канале.
Каждый класс интеграции является реализацией абстрактного класса
базовой интеграции. Абстрактный класс был применён потому, что некоторые
базовые методы являются общими для большинства интеграций. На рисунке 3.4
представлена диаграмма классов подсистемы интеграции каналов.
44
Рисунок 3.4 – Диаграмма классов подсистемы интеграции каналов
Абстрактный класс AbstractChannel имеет следующие методы:
1)
send_message – выполняет отправку сообщения в канал. Данный
метод выбрасывает исключение в случае неудачи.
2)
get_user_info – выполняет запрос дополнительной информации о
пользователе. Порой не каждый канал пересылает информацию о пользователе
вместе с входящими сообщениями, поэтому данный метод позволяет запросить
эту дополнительную информацию. В случае, если канал не поддерживает запрос
дополнительной
информации,
выбрасывает
исключение
типа
NotImplementedException [17].
3)
get_location – данный метод осуществляет запрос геопозиции
пользователя. В случае, если это невозможно в данном канале, выбрасывает
исключение типа NotImplementedException.
4)
start – осуществляет инициализацию интеграции канала. Здесь канал
может авторизоваться в канале (токен [18]), запустить специфический для канала
клиент и т.д.
5)
feed – осуществляет отправку сообщения в единую очередь
обращений
От данного абстрактного класса наследуются все интеграции каналов.
Классы, реализующие интеграцию каналов, обязаны реализовать методы
send_message, get_user_info, get_location и start. Таким образом, классы данного
45
семейства отвечают за отправку сообщений в канал, а также за запрос
дополнительной информации о клиенте.
Также на диаграмме классов представлены EmailListener и HttpListener,
связанные с классами EmailChannel и TelegramChannel соответственно. Таким
образом на диаграмме классов отображена связь класса канала и способа
получения сообщений из канала.
Алгоритм работы метода send_message представлен на рисунке 3.5.
Начало
Поддерживается ли тип
сообщения каналом?
Нет
Да
Конвертация внутрисистемного
сообщения в тип канала
Отправка сообщения
в канал
Конец
Рисунок 3.5 – Алгоритм работы функции send_message
Метод получения входящих сообщений из канала сильно зависит от
интегрируемого канала, однако помимо способа приёма сообщений, алгоритм
работы остается общим для всех каналов. Алгоритм работы изображен на рисунке
3.6.
46
Начало
Конвертация сообщения
в соответствии с его типом
Отправка в
единую очередь
Конец
Рисунок 3.6 – Алгоритм приёма входящего сообщения
3.3 Проектирование подсистемы маршрутизации сообщений
Подсистема маршрутизации сообщений состоит следующих модулей:
1)
Модуль постановки в очередь – этот модуль реализует интерфейсы
для подсистемы интеграции каналов, с помощью которых данная подсистема
сможет производить постановку прибывших сообщений в очередь;
2)
Модуль маршрутизации – этот модуль осуществляет маршрутизацию
и управление маршрутом сообщений. Также данный модуль отвечает за
реализацию алгоритмов маршрутизации, реализацию HTTP интерфейсов для
асинхронных ответов агентов.
3)
Модуль оперативной базы данных – модуль отвечает за управление
оперативной базой данных. Содержит интеграционные интерфейсы, с помощью
которых остальные подсистемы могут запрашивать данные из оперативной базы
данных, реализует атомарные функции работы с единой очередью сообщений,
оперативными данными о диалогах, клиентах и сообщениях.
4)
Модуль архивной базы данных – модуль реализует функции,
облегчающие работу с архивной базой данных. Здесь происходит создание,
чтение, обновление, удаление (CRUD [19]) сущностей, которые уже не требуются
для работы полного конвейера системы. К таким сущностям относятся:
завершенные
обращения,
история
оперативной базы данных и т.д.
сообщений,
клиенты,
удалённые
из
47
На рисунке 3.7 изображена архитектура подсистемы маршрутизации.
Подситема диспетчеризации
История обращений
Неактивые клиенты
Статистика
Обмен сообщениями
Запросы обращений и пр.
Подсистема маршрутизации
Архив.
БД
Опер.
БД
Модуль
архивной
БД
Запросы клиентов
и истории обращений
Модуль
маршрутизации
Модуль
оперативной
БД
Получение
новых сообщений
Очередь сообщений
Активные обращения
Клиенты
Системная информация
Конфигурация
Запросы архивных и
статистических данных
Отправка
исходящих
сообщений
Отправка
сообщений
Разборщики очереди
Модуль API для
внешних систем
Постановка
в очередь
Модуль
постановки в очередь
Отправка
входящих
сообщений
Подсист.
интеграции
каналов
Рисунок 3.7 – Архитектура подсистемы маршрутизации
Для эффективной реализации данной подсистемы, было выделено два типа
баз данных: оперативная и архивная. Обе базы данных являются базами NoSQL
[20] базами данных, однако каждая имеет свою специфику.
Оперативная база данных – это обязательно база данных в памяти (in
memory database [21]). В такой базе данных запросов записи и чтения примерно
поровну. Необходимо обеспечить высокую скорость работы всех узлов конвейера
сообщений, что положительно скажется на общей пропускной способности
системы. Такие базы данных обычно имеют вид ключ-значение [21]. Базы данных
в памяти обычно реализуют некоторые традиционные структуры данных, такие
как: сортированное множество (sorted set), хеш-таблица, список и так далее.
Использование готовых отлаженных реализаций абстрактных типов данных
позволит сократить время разработки. Еще одним преимуществом таких баз
данных является их способность к горизонтальному масштабированию и
репликации. Оперативная база данных обязательно должна обеспечивать
персистентность (persistency [21]). Так как все данные базы данных загружаются в
память, пропускная способность (выраженная в количестве запросов в секунду)
такой базы данных будет очень высокой. Здесь предполагается хранить единую
48
очередь сообщений, список активных обращений, списки сообщений из активных
обращений, списки клиентов.
Нужно
отметить,
что
оперативная
база
данных
является
некой
«оперативной памятью» для подсистемы маршрутизации. Таким образом, даже
если будет запущено несколько экземпляров подсистем маршрутизации, они
будут
иметь
общую
память,
что
является
отличным
подспорьем
для
горизонтального масштабирования.
Архивная база данных – это база данных, основной целью которой является
хранение больших объемов информации, при этом количество запросов чтения
гораздо больше, чем запросов записи. База данных должна иметь встроенный
механизм запросов, позволяющий делать выборки, фильтровать и сортировать
данные. Отслеживание связей между сущностями и поддержание ссылочной
целостности не требуется, в виду специфики задачи. Здесь происходит хранение
архивных обращений, истории сообщений, клиентов без активных обращений,
историю сессий.
Для хранения очереди сообщений будем использовать оперативную базу
данных. Постановка в очередь является тривиальной атомарной операцией.
Сложность возникает при реализации разборщиков очереди, поскольку важно
использовать как можно меньше времени оперативной базы данных, для того
чтобы не блокировать выполнение других запросов. Для решения этой задачи
подходит шаблон проектирования Producer-Consumer [12].
Множество каналов, из которых сообщения поступают в единую очередь –
это множество изготовителей (producer). Для реализации исполнителей будем
использовать концепцию разборщиков очереди (consumer). Каждый разборщик
очереди исполняется в самостоятельном потоке и использует собственное TCPподключение к оперативной базе данных.
Схематичное
изображение
данного
шаблона
проектирования,
применительно к разрабатываемой подсистеме, представлено на рисунке 3.8.
49
Канал
Канал
Канал
Очередь сообщений
Разборщик Разборщик Разборщик
Рисунок 3.8 – Producer-Consumer в разрабатываемой подсистеме
При использовании producer-consumer мы имеем возможность сглаживать
пики нагрузки на систему, так как операция постановки в очередь является очень
дешевой по сравнению с операцией обработки сообщения из очереди. По
завершению обработки, разборщик очереди проверяет наличие сообщений в
очереди. Если сообщений нет, разборщик входит в режим ожидания. Необходимо
обеспечить атомарность операций изъятия из очереди, никакие две операции в
оперативной базе данных не должны выполняться одновременно.
Такой шаблон проектирования также позволяет нам объединять множество
подсистем маршрутизации в единую распределенную систему, так как очередь
хранится в оперативной базе данных, поэтому память среди подсистем будет
общей.
Хранилище в данной подсистеме является двухуровневым. Оперативные
данные, необходимые для критичных участков системы, хранятся в оперативной
базе данных. Если данные уже не нужны, они выгружаются в архивную базу
данных.
3.4 Проектирование подсистемы диспетчеризации
Подсистема диспетчеризации отвечает за назначение обращений на
операторов.
рисунке 3.9.
Архитектура
подсистемы
диспетчеризации
изображена
на
50
Очередь обращений
Операторы, их
состояний
Модуль
оперативной
базы данных
Модуль управления
статусами операторов
Интерфейс агента
Модуль диспетчеризации
БД
опер.
Обмен сообщениями
Подсистема диспетчеризации
Запрос обращений
Модули
БД
Модуль маршрутизации
Рабочее
место
оператора
(операторы,
менеджеры)
Интеграция
Рабочего
Места
оператора
Отправка статистики
статусов операторов
...
Подсистема маршрутизации
Рисунок 3.9 – Архитектура подсистемы диспетчеризации
Модуль интеграции рабочего места оператора – данный модуль реализует
интерфейс как для самого рабочего места оператора, так и для модуля
диспетчеризации
и
модуля
управления
статусом
операторов.
Модуль
диспетчеризации должен иметь возможность сообщить рабочему месту оператора
о новых сообщениях, а модуль управления статусом операторов об изменениях
статусов. Интеграция с модулями подсистемы осуществляется программными
средствами с помощью событий, интеграция с подсистемой рабочего места
оператора реализуется посредством HTTP.
Модуль управления статусами оператора – данный модуль отвечает за
реализацию функций изменения статуса оператора, контроль за корректностью
переходов между статусами, а также отправку статистической информации
статусов операторов в архивную базу данных подсистемы маршрутизации.
Оператор не может одновременно находится в нескольких статусах, статусы
являются взаимоисключающими.
Модуль
оперативной
базы
данных.
Модуль
реализует
функции,
облегчающие работу с оперативной базой данных. В оперативной базе данных
хранятся: операторы, статусы операторов, очередь обращений, кэш сообщений.
Модуль
маршрутизации
интерфейса
агента
посредством
–
реализует
реализации
интеграцию
единого
подсистемы
интерфейса
агентов.
51
Сообщения, попав на данный интерфейс кэшируются, а затем осуществляется
диспетчеризация сообщения на оператора.
Модуль диспетчеризации реализует логику разбора очереди обращений,
логику назначения обращений на операторов, а также интерфейсы для перевода
обращения, изменения параметров диспетчеризации.
Поскольку каждый оператор контакт-центра имеет разный уровень
квалификации, разный качественный и количественный уровень знания языков,
уровень знания предметной области, то обращения должны поступать самому
компетентному оператору по тематике обращения. Выделим возможные
компетенции:
−
линия;
−
язык;
−
заказчик;
−
навык (тематика).
Параметры диспетчеризации обращения состоят из значений справочников
для каждой из компетенций. Также введём специальное обозначение для
компетенций – “ANY”, обозначающая любую из существующих значений для
компетенций. Когда обращение только поступило в систему, используется набор
компетенций по умолчанию, например 1-ая линия в техподдержке. На рисунке
3.10 изображен пример взаимосвязи операторов и компетенций.
Компетенции
Англ. язык
1-я линия
2-я линия
...
Операторы
Рисунок 3.10 – Пример взаимосвязи операторов и компетенций
Согласно этим параметрам диспетчеризации, формируются множества
операторов, по множеству на каждое значение справочника компетенций.
Подробнее это описано далее.
52
В базе данных хранятся данные операторов, а также вспомогательные
данные:
−
список операторов, зарегистрированных для диспетчеризации;
−
множество операторов с оценками диспетчеризации;
−
соответствие операторов и их статусов;
−
множества операторов по компетенциям;
−
списки назначенных диалогов на операторов.
Перевод обращения на другого оператора заключается в изменении
параметров маршрутизации обращения, удалении ассоциации обращения и уже
назначенного оператора, а затем снова постановке обращения в очередь
диспетчеризации.
3.5 Проектирование алгоритмов маршрутизации
Разборщик очереди, получив очередное сообщение, вызывает функцию
обработки входящих сообщений. Выполнение данной функции состоит из
нескольких этапов.
На первом этапе сообщение инициализируется необходимыми для работы
данными: происходит установка временных меток, устанавливается начальная
маршрутизация сообщения.
Следующим этапом, проверяется наличие клиента в базах данных.
Изначально проверяется только оперативная база данных, затем если в
оперативной базе данных клиента нет, то производится запрос в архивную базу
данных. Если в итоге клиент не был найден, производится создание нового
клиента и присваивание ему уникального омниканального идентификатора. В
случае, если обращение еще не существует, производится запрос данных клиента
из канала. Таким образом, данные клиента обновляются раз за обращение.
Третий этап – это проверка на обращения существования. В случае, если
обращения еще нет в оперативной базе данных, то оно создается. Полученное
обращение проходит этап инициализации, установки начальных компетенций, а
также установки статистических данных и временных меток.
53
На четвертом этапе проверке на существование подвергается сессия. После
получения или создания сессии, устанавливаются статистические данные и
временные метки сессии.
После
этапов
инициализации,
подгружается
конфигурация
агентов
заказчика и производится вызов первого агента в цепочке. На рисунке 3.11
представлен общий алгоритм обработки входящих сообщений.
Начало
Установка
временных меток
сообщения
Нет
Клиент существует?
Да
Загрузить из БД
Создать клиента,
используя данные
из канала
Нет
Новое
обращение?
Да
Обновить
данные
клиента
из канала
Нет
Обращение существует?
Да
Загрузить из БД
Создать обращение
Обновить данные
статистики обращения
Нет
Сессия существует?
Да
Создать сессию
Обновить данные
статистики сессии
Загрузить конфигурацию
агентов
Выполнить цепочку
агентов
Конец
Рисунок 3.11 – Общий алгоритм обработки входящего сообщения
54
После того, как сообщение прошло подготовительный этап, выполняется
функция вызова агента. Алгоритм вызова агента представлен на рисунке 3.12.
Начало
Кеширование сообщения
и состояния цепочки
Вызов агента
Вызов
успешен?
Да
Нет
Есть ли
аварийный агент?
Обработка
ответа агента
Да
Нет
Конец
Рисунок 3.12 – Алгоритм вызова агента
В начале работы алгоритма происходит кеширование сообщения и общего
состояния цепочки (идентификатора текущего агента). Это необходимо для
корректной обработки асинхронных вызовов подсистемы маршрутизации
агентами.
Далее, происходит непосредственно сам вызов агента посредством HTTP
запроса. Агент может как ответить на запрос сразу, синхронно, так и не ответить в
принципе в виду различных ошибок в сети или разработанных подсистемах. В
таком случае, для каждого агента в цепочке предусмотрен аварийный агент,
указываемый при конфигурировании цепочки. В случае, если такой агент указан,
то будет произведен повторный вызов, влекущий за собой повтор алгоритма.
В случае, если вызов агента был успешен, и агент ответил сообщением, то
производится вызов функции обработки ответа агента. На рисунке 3.13
представлен алгоритм обработки ответа агента.
55
Начало
Нет
Ответ агента
содержит
перенаправление?
Записать сообщение
в БД
Да
Агент существует?
Обновить временные
метки и статистическую
информацию диалога
и сессии
Нет
Да
Выполнить вызов
агента
Выбросить исключение
Конец
Рисунок 3.13 – Алгоритм обработки ответа агента
Для обеспечения гибкости разрабатываемой системы, агенты могут
динамически перенаправлять сообщения на других агентов. Поэтому, сообщение
ответа агента может содержать специальные команды вида “/redirect <agent_id>
<message>”, которые производят перенаправление сообщения на указанного
агента.
В случае, если перенаправление в сообщении присутствует, и агент с
указанным идентификатором существует в конфигурации заказчика, то снова
выполняется функция вызова агента. Если же такого агента в конфигурации нет,
то выбрасывается исключение.
Если же сообщение не содержит команды перенаправления, то сообщение
записывается в оперативную базу данных, устанавливаются временные метки и
обновляется статистическая информация диалога.
3.6 Проектирование алгоритмов диспетчеризации
У каждого оператора имеется список компетенций, используемый для
диспетчеризации обращений. При регистрации оператора на диспетчеризацию,
56
происходит добавление оператора в множества по компетенциям, по одному
множеству на компетенцию. Также формируется сортированное множество из
всех операторов в системе, при этом, для сортировки данного множества
используется специальный числовой показатель – оценка.
Оценка оператора необходима для реализации честной диспетчеризации.
Критерии честной диспетчеризации:
−
наиболее свободный оператор получает новые обращения в первую
очередь;
−
среди одинаково свободных операторов новое обращение получает
тот, на которого обращения поступали наиболее давно.
Следовательно, необходима числовая оценка оператора. Такую оценку
можно представить в виде:
 =  ∙  + ,
(3.1)
где s – оценка оператора;
C – это коэффициент значимости количества обращений,  > ;
d – количество обращений, которые еще может принять оператор, данное
значение едино для всех операторов заказчика;
t – временная метка.
Функция диспетчеризации реализована в оперативной базе данных для
атомарного выполнения. Атомарное выполнение необходимо для выполнения
требования распределённости системы.
На
рисунке
3.14
изображен
алгоритм
диспетчеризации.
Алгоритм
диспетчеризации начинается с поступления обращения из очереди. На основе
параметров диспетчеризации обращения, формируется список компетенций.
Основываясь на списке компетенций, выбираются множества операторов по
компетенциям, а затем пересекаются. В итоге образуется множество операторов,
подходящих по компетенции данному обращению.
57
Начало
Поступление обращения
из очереди
Формирование списка
компетенций
Пересечение множеств
операторов по компетенциям
Фильтр операторов
на основе получившегося
множества
Выбор оператора с
максимальной оценкой
Оператор найден?
Да
Нет
Вернуть обращение
в очередь
Конец
Рисунок 3.14 – Алгоритм диспетчеризации
Далее,
общее
сортированное
множество
операторов
с
оценками
фильтруется на основе множества операторов, полученного пересечениями. Из
получившегося множества операторов с оценками выбирается оператор с
максимальной оценкой.
Для облегчения восприятия данного алгоритма, был составлен пример
работы, представленный на рисунке 3.15.
58
Обращение
с компетенциями:
S1, S2, S4, S5
Мн-во
Операторы
S1
1, 2, 3, 4
S2
2, 3, 4
S3
1, 2, 3 ,5
S4
1, 2, 3, 4
S5
2, 3, 4
Выбор множеств
по компетенциям
Пересечение множеств:
S1∩S2∩S4∩S5
=
Операторы
2, 3
Очередь
Опер.
Оценка
1
0
2
2000
3
1000
Выборка
операторов
2, 3
и оценок
4
3000
op.score > 0
5
0
max [op.score]
Опер.
Оценка
2
2000
3
1000
Оператор
2
Рисунок 3.15 – Пример работы алгоритма диспетчеризации
59
4 ОСОБЕННОСТИ РЕАЛИЗАЦИИ ПРОГРАММНОЙ СИСТЕМЫ
ИНТЕГРАЦИИ И ДИСПЕТЧЕРИЗАЦИИ КАНАЛОВ РАЗНОРОДНЫХ
СИСТЕМ ОБМЕНА СООБЩЕНИЯМИ
4.1 Используемые библиотеки и технологии
В качестве основного языка разработки был выбран функциональный язык
F# [22]. Язык позиционируется как функциональный, однако на самом деле
является мультипарадигмальным, используя при этом всю мощь платформы .NET
[23]. Платформа .NET имеет множество готовых библиотек для самых различных
целей. Язык компилируется в специальный байт-код, называемый Internal
Language (IL [24]), а затем данный байт-код компилируется с помощью JITкомпиляции в инструкции процессора, на котором исполняется программа. В
языке F# возможно как программирование в функциональном стиле, так и в
императивном, также возможно использование объектно-ориентированного
подхода.
Разработка системы велась в интегрированной среде разработки JetBrains
Rider [25]. Данная среда была выбрана в связи с удобством единого интерфейса
продуктов JetBrains, а также наличием встроенного богатого функционала
отладчика.
Для управления зависимостями системы использовался менеджер пакетов
NuGet. Все разрабатываемые сервисы реализовали REST API, поэтому был
необходим пакет, реализующий
HTTP-сервер. В качестве программного
интерфейса веб-сервера был использован стандарт OWIN [26] и реализация
данного
стандарта
в
виде
пакетов
Microsoft.Owin.Hosting
и
Microsoft.Owin.Host.HttpListener. Также в интегрированной среде разработки
поддерживается управление пакетами NuGet.
Для организации юнит- и функционального тестирования использовалась
библиотека NUnit. Данная библиотека позволила существенно сократить время
разработки тестов, а среда разработки позволяла запускать тесты, получать
отчеты о выполнении и оценивать время работы каждого теста.
60
Так как было реализовано именно REST API, единым форматом общения
между подсистемами был выбран JSON [27]. Данный формат представления
данных является понятным человеку, что позволяет не тратить время на
дополнительное декодирование информации при отладке.
Конфигурация всех подсистем осуществлялась с помощью JSON-файлов,
содержащих
объекты
конфигураций.
Пример
файла
конфигурации
для
подсистемы интеграции каналов в нотации JSON:
[
{
"customer_id": "test",
"channel_type": "webchat",
"title": "Webchat Test Channel",
"channel_id": "webchat_test",
"id_in_channel": "webchat_test",
"webhook_host": "http://localhost:10002/webhook",
"parameters": []
},
{
"customer_id": "test",
"channel_type": "testbot",
"title": "TestChannel",
"channel_id": "TestChannel",
"parameters": []
},
…
]
В качестве оперативной базы данных была выбрана база данных Redis [28].
Это база данных типа ключ-значение, с хранением в памяти. База данных
написана на языке C и является однопоточной. В этой базе данных уже
реализованы многие абстрактные типы данных, поддерживается выполнение
пользовательских сценариев на языке Lua. Это однопоточное приложение,
способное выдавать до сотен тысяч запросов в секунду. Также Redis имеет
хорошие способности к горизонтальному масштабированию. Поддерживается
репликация
вида
master-slave.
Redis
использует
собственный
протокол,
основанный на TCP.
Используется библиотека StackExchange.Redis для взаимодействия с Redisсервером. Это высокопроизводительный redis-клиент общего применения для
.NET языков. Особенности данной библиотеки:
61
−
реализует высокопроизводительную мультиплексную архитектуру,
позволяющую
эффективно
использовать
общее
TCP
соединение
для
использования многими потоками;
−
абстрагирует
конфигурацию
узлов
redis:
клиент
может
самостоятельно выбирать наиболее доступный узел;
−
предоставляет удобный доступ к полному набору команд redis;
−
поддерживает
две
модели
программирования:
синхронную
и
асинхронную;
−
В
реализует поддержку Redis Cluster.
качестве
архивной
базы
данных
данных
ориентированная
база
распределённая,
REST-ориентированная
ElasticSearch
была
выбрана
[29].
ElasticSearch
поисковая
документо-
система,
–
это
решающая
множество задач. Документы хранятся в индексах, в каждом индексе возможно
хранить лишь один тип документов. ElasticSearch позволяет выполнять
агрегацию, фильтрацию и прочие операции на большом объёме данных. В
качестве клиента ElasticSearch была использована официальная реализация
Elasticsearch.Net.
4.2 Реализация интеграции канала на примере канала E-Mail
Канал E-Mail был выбран для примера в качестве одного из самых
неординарных каналов для интеграции. Для реализации канала E-Mail был
разработан специальный класс EMailChannel, наследующийся от абстрактного
класса AbstractChannel.
Для
приёма
входящих
сообщений
использовался
IMAP-клиент,
реализованный библиотекой S22.ImapWithUTF8. Данный IMAP-клиент был
выбран после продолжительных испытаний различных IMAP и POP3 клиентов.
Наиболее надежной и отказоустойчивой оказалась реализация именно с помощью
данной библиотеки.
Необходима инициализация данной интеграции с помощью метода start().
На рисунке 4.1 приведен алгоритм работы функции инициализации.
62
Начало
Получен сигнал
остановки канала?
Да
Нет
Получение конфигурации
канала
Подключение к
IMAP-серверу
Поиск новых
сообщений
Для каждого
сообщения
Извлечение вложений,
определение mime-типа
Вызов функции обработки
нового сообщения
Конец
Рисунок 4.1 – Алгоритм функции инициализации канала E-Mail
Из схемы данного алгоритма следует, что цикл проверки E-Mail сообщений
выполняется на протяжении всего времени работы подсистемы интеграции.
Функция
инициализации
каждого
канала
вызывается
при
старте
подсистемы интеграции. Для того, чтобы канал E-Mail мог подключиться к IMAPсерверу, необходимо предоставить авторизационные данные. Это реализуется при
помощи списка объектов типа Pair в поле parameters объекта класса ChannelInfo.
Пример конфигурации канала E-Mail:
{
"customer_id": "test",
"channel_type": "email",
"title": "Email Channel",
"channel_id": "email_test",
"id_in_channel": "[email protected]",
"webhook_host": null,
63
"parameters": [
{"n": "imap_host", "v": "imap.yandex.ru"},
{"n": "imap_port", "v": "993"},
{"n": "imap_login", "v": "[email protected]"},
{"n": "imap_password", "v": "123456"},
{"n": "smtp_host", "v": "smtp.yandex.ru"},
{"n": "stmp_port", "v": "587"},
{"n": "smtp_login", "v": "[email protected]"},
{"n": "smtp_password", "v": "123456”},
…
]
}
Все параметры находятся в едином списке и извлекаются из данного списка
с помощью специальных функций-помощников get_param_<type>, реализованных
во вспомогательном модуле Utility.
Для отправки сообщений в канал, используется SMTP-клиент. В качестве
SMTP-клиента
был
использована
реализация,
доступная
в
стандартной
библиотеке: System.Net.Mail.SmtpClient.
Дополнительной сложностью при интеграции E-Mail оказался тот факт, что
каждый E-Mail клиент кодирует вложения по-разному. Возможные виды способов
вложений:
−
с помощью HTML-тега img в тексте письма;
−
с помощью Alternative Views вложений в кодировке base64;
−
с помощью Attachments.
Необходимо преобразовывать каждое вложение в отдельное сообщение во
внутрисистемном формате, а затем отправлять их в единую очередь сообщений.
4.3 Особенности реализации модуля маршрутизации
Для реализации постановки в очередь, использовался тип данных Redis List,
представляющий собой список. Для постановки в очередь использовалась
команда вида RPUSH <key> <data>. Данная команда осуществляет добавление в
начало списка по ключу <key> данные <data>.
Для того, чтобы реализовать систему producer-consumer с помощью Redis,
использовался следующий подход. Была реализована специальная функция,
представляющая собой разборщик очереди. Каждый такой разборщик запускается
64
в отдельном потоке, чтобы максимально использовать ресурсы процессора.
Также, на каждый разборщик выделяется по одному TCP-подключению к базе
данных Redis. Количество таких разборщиков конфигурируется в JSON-файле
конфигурации подсистемы маршрутизации в поле “workers”. На рисунке 4.2
представлена схема такого взаимодействия.
Redis
TCP-соединения
BLPOP
BLPOP
Разборщик
BLPOP
Разборщик
Разборщик
Рисунок 4.2 – Взаимодействие Redis и разборщиков очереди
Затем, каждый разборщик осуществляет запрос к Redis базе данных
используя команду BLPOP. Данная команда является блокирующей, поэтому
выделяется отдельное
TCP-соединение. Поток управления переходит из
пользовательского кода в библиотеку, и не возвращается до тех пор, пока в
очереди не появятся данные для обработки. При этом, поток, в котором
происходит выполнение данного запроса, освобождается для других задач. В
итоге, мы получаем эффективную реализацию шаблона producer-consumer с
помощью базы данных Redis.
Приведём пример запроса клиента из архивной базы данных ElasticSearch.
Был разработан специальный DSL [30] для облегчения взаимодействия с этой
базой данных на языке F#. Код функции, осуществляющей поиск в архивной базе
данных клиентов по omni_user_id:
let GetUser(omni_user_id : string) : User =
let q =
Search [
Size 1;
Query (
65
Bool [
Must [ Term ("omni_user_id", [ExactValue
omni_user_id]) ]
]
)
]
Elastic.Search<User> connection q |> Async.RunSynchronously
Как видно из данного отрывка, разработанный язык запросов сильно
упрощает составление запросов, поскольку код, набранный таким образом
позволяет заранее заметить ошибку в запросе. Также с помощью данного DSL
наглядно видна семантика каждого блока запроса. Это достигается с помощью
использования конструкции языка Discriminated Union (DU [30]). Получившийся
затем объект типа Query преобразуется в строковый JSON с помощью
сопоставления с образцом (pattern-matching [30]). Для примера приведём
аналогичный JSON:
{
"size":1,
"query":{
"bool":{
"must":{
"term":{
"omni_user_id":"c9518b27-7608-4b1e-a224-a68278180a70"
}
}
}
}
}
Очевидно, при помощи данного DSL мы получаем преимущества в виде:
−
подсветки кода на этапе разработки;
−
анализа запроса на этапе компиляции;
−
исключён шанс случайной опечатки.
4.4 Особенности реализации модуля диспетчеризации
Главной особенностью модуля диспетчеризации является реализация
алгоритма диспетчеризации. Как было упомянуто выше, в основе данного
алгоритма лежит пересечение множеств, а также выборка из множеств.
Необходима эффективная реализация данных операций. Выбранная ранее база
66
данных Redis включает эффективную реализацию необходимых абстрактных
типов данных, а также операций над ними.
В качестве множеств будем использовать типы данных базы данных Redis –
set и sorted set, а также list. Set представляет собой реализацию множества,
основанную на хэш-таблице. Sorted set реализуется посредством одного из видов
сбалансированных двоичных деревьев поиска. List – обычный список.
Для хранения очереди обращений будем использовать тип данных List. Для
добавления обращения в конец очереди, будем использовать команду Redis
LPUSH. Для изъятия обращения с вершины очереди, используется команда RPOP.
Схематично это представлено на рисунке 4.3.
LPUSH
D1 D2 D3
RPOP
Очередь обращений
Рисунок 4.3 – Визуализация команд Redis для работы с очередью
Для организации хранения множеств операторов по компетенциям,
наилучшим образом подойдёт тип данных set. После того, как оператор
зарегистрируется в системе, необходимо добавить оператора в каждое из
множеств его компетенций. Для добавления оператора во множества будем
использовать команду SADD.
Аналогично, когда оператор завершает работу в системе, он изменяет свой
статус на «не в сети». После этого, необходимо снова удалить оператора из
множеств по компетенциям, чтобы не происходило ложных диспетчеризаций на
операторов, находящихся не в сети. Для удаления оператора из множества будем
использовать команду SREM.
Для выполнения операции пересечения множеств, необходимо использовать
вспомогательное множество, куда будет производиться пересечение. По
окончанию работы алгоритма диспетчеризации, для корректной работы алгоритма
в последующем, необходимо обязательно удалить все побочные множества.
Удаление любого ключа в базе данных Redis осуществляется с помощью команды
67
DEL <key>. Сама операция пересечения множеств будет осуществляться с
помощью команды SINTERSTORE <destination> <key> [<key> …].
Для
корректной
диспетчеризации,
необходимо
чтобы
алгоритм
диспетчеризации всегда выполнялся атомарно. Для этого в базе данных Redis
существует специальный механизм, позволяющий выполнять пользовательские
скрипты, написанные на языке Lua, в эксклюзивном режиме прямо на сервере.
Поэтому, алгоритм диспетчеризации был реализован изначально на языке Lua.
Использовать несколько языков не всегда удобно, поэтому было решено
воспользоваться специальной надстройкой для языка F# - quotation expressions,
позволяющую реализовывать трансляцию конструкций языка F# прямо во время
выполнения. Был разработан небольшой транслятор F# в Lua, в связи с чем код
алгоритма диспетчеризации стало проще отлаживать, а также появилась защита
от случайной ошибки. Приведём выдержку из кода алгоритма диспетчеризации:
let setSize : int = ~~~R.Execute("ZCARD", [|opSetKey|])
if setSize > 0 then
let tempSetKey2 = &&"tempSetKey" + "_ops"
R.Execute("ZINTERSTORE", [|tempSetKey2; string 2;
&&"tempSetKey"; opSetKey |])
|> ignore
let setSize : int = ~~~R.Execute("ZCARD", [|tempSetKey2|])
if setSize > 0 then
R.Execute("RENAME", [|tempSetKey2; &&"tempSetKey"|]) |>
ignore
Реализованный алгоритм диспетчеризации имеет асимптотическую оценку
O(N*K), где N – количество операторов, зарегистрированных в системе, K –
количество компетенций обращения.
68
ЗАКЛЮЧЕНИЕ
Задача разработки программной системы интеграции и диспетчеризации
каналов разнородных систем обмена сообщениями является актуальной,
существует огромное подспорье для экспериментирования с различными
подходами и методами интеграции и диспетчеризации каналов. В рамках данной
выпускной
подсистема
работы
были
маршрутизации
разработаны
сообщений
подсистема
и
интеграции
подсистема
каналов,
диспетчеризации
обращений.
В ходе выполнения выпускной квалификационной работы были решены все
поставленные задачи. Была охарактеризована и формализована задача интеграции
и диспетчеризации каналов разнородных систем обмена сообщениями. Был
проведен анализ предметной области и на основе этого были составлены
требования к системе.
На основе поставленных требований были разработаны спецификации к
разрабатываемому программному обеспечению. Была разработана архитектура и
спроектирована структура. Были спроектированы структуры данных и алгоритмы.
Произведён выбор языковых и инструментальных средств разработки, были
разработаны модули программного обеспечения и протоколы обмена данными.
Было проведено тестирование и отладка программного обеспечения.
В заключении, можно сделать вывод о том, что поставленная цель была
достигнута.
69
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ И ЛИТЕРАТУРЫ
1.
Омниканальность: 7 советов маркетологу [Электронный ресурс]. –
Режим доступа: https://habr.com/company/infobip/blog/336048/. – Дата доступа:
01.06.2018.
2.
Силен Д., Основы Data Science и Big Data. Python и наука о данных
[Текст] / Д. Силен, А. Мейсман, М. Али – СПб.: Питер, 2017. – 336 с.
3.
Ипатьева И.А. Развитие омниканальных сбытовых систем и их
адаптация на российском рынке [Электронный ресурс] // VII Международная
студенческая электронная научная конференция «Студенческий научный форум»
- 2015. – Режим доступа https://www.scienceforum.ru/2015/939/7581. – Дата
доступа: 01.06.2018.
4.
Будущее контакт-центров: омниканальность и клиентский опыт
[Электронный ресурс]. – Режим доступа: https://habr.com/company/cti/blog/335670/.
– Дата доступа: 01.06.2018.
5.
Официальный сайт OkTell [Электронный ресурс]. – Режим доступа:
https://oktell.ru/callcenter/chatcenter/. – Дата доступа: 01.06.2018.
6.
Официальный сайт LiveTex [Электронный ресурс]. – Режим доступа:
https://livetex.ru. – Дата доступа: 01.06.2018.
7.
Официальный сайт RedHelper [Электронный ресурс]. – Режим
доступа: https://redhelper.ru/. – Дата доступа: 01.06.2018.
8.
Официальный сайт JivoSite [Электронный ресурс]. – Режим доступа:
https://www.jivosite.ru/. – Дата доступа: 01.06.2018.
9.
Официальный сайт Naumen Omnichannel [Электронный ресурс]. –
Режим доступа: https://www.naumen.ru/products/phone/tour/features/omnichannel/. –
Дата доступа: 01.06.2018.
10.
Фаулер М. UML. Основы. Краткое руководство по стандартному
языку объектного моделирования – Москва: Символ-Плюс, 2018. – 192 с.
70
11.
Вигерс М. Разработка требований к программному обеспечению. 3-е
изд., дополненное / Пер. с англ. — М. : Русская редакция, СПб. : БХВПетербург, 2014. — 736 с.
12.
Фаулер М. Архитектура корпоративных программных приложений. –
Москва : Вильямс, 2007. – 544 с.
13.
Документация Bot API Telegram [Электронный ресурс]. – Режим
доступа: https://core.telegram.org/bots/api. – Дата доступа: 01.06.2018.
14.
Bots: An introduction for developers [Электронный ресурс]. – Режим
доступа: https://core.telegram.org/bots#3-how-do-i-create-a-bot. – Дата доступа:
01.06.2018.
15.
Официальный сайт VK [Электронный ресурс]. – Режим доступа:
https://vk.com. – Дата доступа: 01.06.2018.
16.
Официальный сайт Facebook [Электронный ресурс]. – Режим доступа:
https://facebook.com. – Дата доступа: 01.06.2018.
17.
Класс NotImplementedException [Электронный ресурс]. – Режим
доступа: https://msdn.microsoft.com/ruru/library/system.notimplementedexception(v=vs.110).aspx. – Дата доступа:
01.06.2018.
18.
The OAuth 2.0 Authorization Framework [Электронный ресурс]. –
Режим доступа: https://tools.ietf.org/html/rfc6749. – Дата доступа: 01.06.2018.
19.
Parastatidis S., Webber J., Robinson I. REST in Practice – NY : O'Reilly
Media, 2013. – 406 с.
20.
Фаулер М., Дж. Садаладж П. NoSQL. Новая методология разработки
нереляционных баз данных – Москва : Вильямс, 2013. – 192 с.
21.
Клеппман М. Высоконагруженные приложения. Программирование,
масштабирование, поддержка – СПб : Питер, 2018 – 640 с.
22.
F# Software Foundation [Электронный ресурс]. – Режим доступа:
https://fsharp.org/. – Дата доступа: 01.06.2018.
23.
Официальный сайт .NET [Электронный ресурс]. – Режим доступа:
https://www.microsoft.com/net/. – Дата доступа: 01.06.2018.
71
24.
Рихтер Дж. CLR via C#. Программирование на платформе
Microsoft.NET Framework 4.5 на языке C# - СПб : Питер, 2017 – 896 с.
25.
Официальный сайт JetBrains Rider [Электронный ресурс]. – Режим
доступа: https://www.jetbrains.com/rider/. – Дата доступа: 01.06.2018.
26.
OWIN – Open Web Interface for .NET [Электронный ресурс]. – Режим
доступа: http://owin.org/. – Дата доступа: 01.06.2018.
27.
Официальный сайт JSON [Электронный ресурс]. – Режим доступа:
https://www.json.org/. – Дата доступа: 01.06.2018.
28.
Официальный сайт Redis [Электронный ресурс]. – Режим доступа:
https://redis.io/. – Дата доступа: 01.06.2018.
29.
Официальная документация Elasticsearch [Электронный ресурс]. –
Режим доступа:
https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html. – Дата
доступа: 01.06.2018.
30.
476 с.
Smith C. Programming F#, Second Edition – NY, O’Reilly Media, 2012 –
72
ПРИЛОЖЕНИЕ А
(обязательное)
ЛИСТИНГ ПРОГРАММЫ
module Email
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
open
AbstractChannel
BotMediatorApi
System
System.Threading
FSharp.Data
System.Net
System.Text
System.Net.Security
System.Security.Cryptography.X509Certificates
System.Net.Http
System.Web.Http
Bots
Newtonsoft.Json
Utility
OwinHelper
Log
S22.Imap
System.Net.Mail
System.IO
FileStorageApi
System.Net.Mime
System.Text.RegularExpressions
type InlineAttachment = {
base64_string: string;
extension: string;
}
let mutable get_bot = fun (id:string) -> setNull<ChannelBot>()
type EmailChannel(channel: ChannelInfo, botMediatorApi: BotMediatorApi,
fileServiceHost : string) =
inherit ChannelBot(channel, botMediatorApi)
let get_mime_type_parts (mime_type: string) =
if mime_type.Contains("/") then
let parts = mime_type.Split([|'/'|], 2)
parts
else [||]
let get_attachment_messages (attachments:AttachmentCollection)
(inline_attachments: InlineAttachment[])
(msg:Bots.Message) : Bots.Message[] =
let msgAttachments = [|
for attachment in attachments ->
let attachment_type =
if not <| String.IsNullOrWhiteSpace
attachment.ContentType.MediaType then
let parts = get_mime_type_parts
attachment.ContentType.MediaType
match parts with
| [| "image"; _ |] -> "IMAGE"
73
| _ -> "FILE"
else "FILE"
let upload = {
fileName = attachment.Name
fileB64 = attachment.ContentStream |> StreamToBytes |>
Convert.ToBase64String
}
{
attachment_type = attachment_type
attachment_url = FileStorageApi.SaveB64 upload fileServiceHost
}
for inline_attachment in inline_attachments ->
let upload = {
fileName = Guid.NewGuid().ToString() + "." +
inline_attachment.extension
fileB64 = inline_attachment.base64_string
}
{
attachment_type = "IMAGE"
attachment_url = FileStorageApi.SaveB64 upload fileServiceHost
}
|]
[|
for attachment in msgAttachments ->
{msg with
id = Guid.NewGuid().ToString()
message =
{msg.message with
text = null
attachment = attachment
}
}
|]
let get_attachment_name (url : string) = Path.GetFileName url
let download_mail_attacment (msg : ReplyMessage) = async {
let web_client = new WebClient()
try
let attachment_url = System.Web.HttpUtility.UrlDecode
msg.message.attachment.attachment_url
let! bytes_array = web_client.DownloadDataTaskAsync(attachment_url) |>
Async.AwaitTask
let attachment_filename = get_attachment_name attachment_url
let attachment_filename' =
if not <|
String.IsNullOrEmpty(web_client.ResponseHeaders.Get("Content-Disposition")) then
web_client.ResponseHeaders.Get("Content-Disposition")
.Substring(web_client.ResponseHeaders.Get("ContentDisposition")
.IndexOf("filename=") + 9).Replace("\"", "");
else attachment_filename
let ext = Path.GetExtension attachment_filename'
let attachment_filename = if String.IsNullOrWhiteSpace(ext) then
attachment_filename else attachment_filename'
let attachment = new Attachment(new MemoryStream(bytes_array),
attachment_filename,
System.Web.MimeMapping.GetMimeMapping(attachment_filename))
return attachment
74
with
| ex ->
log.error "Download mail attachment error: %A" ex
return null
}
let get_reply_message_body (get_param : string -> string) (firstname : string)
(lastname : string) (msg_text : string) =
let message_template = get_param "message_template"
let mutable message_body = Regex(@"%FIRSTNAME%").Replace(message_template,
firstname)
message_body <- Regex(@"%LASTNAME%").Replace(message_body, lastname)
message_body <- Regex(@"%MESSAGE%").Replace(message_body, msg_text)
message_body <- Regex(@"%FOOTER%").Replace(message_body, get_param
"signature")
message_body <- Regex(@"\n").Replace(message_body, "<br>")
message_body
override this.send_message msg channel_user_id user_firstname user_lastname =
if msg.message_type = MessageType.Message then
let get_param = this.get_param
let message_body = get_reply_message_body get_param user_firstname
user_lastname msg.message.text
async {
try
let reply =
new MailMessage(
get_param "email",
msg.session_id,
get_param "subject",
message_body)
reply.From <- new MailAddress(get_param "email", get_param
"sender_name")
reply.IsBodyHtml <- true
let cc_param = this.try_get_param "CC"
if not <| isNull cc_param then
let cc = cc_param.Split([|';'|]) |> Array.filter (fun elem
-> not(String.IsNullOrWhiteSpace elem))
for cc_adress in cc do
let copy = new MailAddress(cc_adress)
reply.CC.Add(copy)
let bcc_param = this.try_get_param "BCC"
if not <| isNull bcc_param then
let bcc = bcc_param.Split([|';'|]) |> Array.filter (fun
elem -> not(String.IsNullOrWhiteSpace elem))
for bcc_adress in bcc do
let copy = new MailAddress(bcc_adress)
reply.Bcc.Add(copy)
if not <| isNull msg.message.attachment
&& not (String.IsNullOrWhiteSpace
msg.message.attachment.attachment_url) then
let! attachment = download_mail_attacment msg
reply.Attachments.Add(attachment)
log.debug "Email channel: outcoming message %A" reply
let client = new SmtpClient(get_param "smtp",
this.get_param_int "smtp_port" 465)
client.DeliveryMethod <- SmtpDeliveryMethod.Network
client.UseDefaultCredentials <- false
75
client.Credentials <- new NetworkCredential(get_param
"smtp_login", get_param "smtp_password")
client.EnableSsl <- this.get_param_bool "enable_ssl" true
System.Net.ServicePointManager.ServerCertificateValidationCallback <(fun _ _ _ _ -> this.get_param_bool
"enable_ssl_certificate_check" true)
do! client.SendMailAsync reply |> Async.AwaitTask
log.debug "Email message sent."
this.send_service_message MessageType.SentConfirmation msg
(setNull<Message>()) channel_user_id |> Async.RunSynchronously
with
| ex ->
log.error "Failed to send message to Email channel: %A" ex
this.send_service_message MessageType.FailedConfirmation msg
(setNull<Message>()) channel_user_id |> Async.RunSynchronously
}
else
async {()}
override this.get_user_info user_id (user:Bots.User) sid = async { return user
}
override this.get_location user_id = setNull<Bots.Location>()
member this.on_new_message (emailMessage:MailMessage) (inline_attachments:
InlineAttachment[]) =
try
log.debug "Incoming message %A" emailMessage
let bot= get_bot(channel.channel_id)
let splitted_display_name = emailMessage.From.DisplayName.Split([|'
'|], 2)
let _firstname, _lastname =
match splitted_display_name with
| [| |] -> "", ""
| [| x |] -> x, ""
| [| x ; y |] -> x, y
| _ -> "", ""
let email_body = Regex(@"(\r\n\s*)+").Replace(emailMessage.Body,
"\r\n")
let message = empty_message()
let message =
{message with
timestamps =
{message.timestamps with
sent_from_channel = unixtime_msec_now() //int64 0
};
channel =
{
channel_id = bot.channel.channel_id;
customer_id = bot.customer_id;
channel_type = "email";
id_in_channel = bot.id_in_channel;
channel_features = [| "TEXT"; "IMAGE"; "FILE" |]
};
user =
{message.user with
channel_user_id = emailMessage.From.Address;
session_id = emailMessage.From.Address
76
firstname = _firstname
lastname = _lastname
phone_number = ""
location = { lat = 0M; long = 0M };
};
message =
{message.message with
text = "Тема: " + emailMessage.Subject + "\n" +
email_body
attachment = setNull<Bots.Attachment>();
action = null
};
}
if not(String.IsNullOrWhiteSpace emailMessage.Subject) &&
not(String.IsNullOrWhiteSpace emailMessage.Body) then
bot.feed(message) |> Async.RunSynchronously
let attachment_messages = get_attachment_messages
emailMessage.Attachments inline_attachments message
for attachment_message in attachment_messages do
bot.feed(attachment_message) |> Async.RunSynchronously
with
| ex -> log.error "%A" ex
override this.start() =
log.debug "Email starting channel bot"
let
let
let
let
imap = base.get_param "imap"
imap_port = this.get_param_int "imap_port" 993
login = base.get_param "imap_login"
password = base.get_param "imap_password"
let rec imapEmailLoop x =
while true do
try
let client = new ImapClient(imap, imap_port, login, password,
AuthMethod.Login, true)
let uids = client.Search(SearchCondition.Unseen())
let messages = client.GetMessages(uids)
for message in messages do
let inline_attachments =
[|
for alt_view in message.AlternateViews ->
let base64 = alt_view.ContentStream |>
StreamToBytes |> Convert.ToBase64String
let mime_type_parts =
if not(String.IsNullOrWhiteSpace
alt_view.ContentType.MediaType) then
get_mime_type_parts
alt_view.ContentType.MediaType else [||]
match mime_type_parts with
| [| "image"; ext |] -> { base64_string =
base64; extension = ext }
| _ -> setNull<InlineAttachment>()
|] |> Array.filter (fun elem -> not(isNull elem))
async { this.on_new_message message inline_attachments} |>
Async.Start
for uid in uids do
client.AddMessageFlags(uid, client.DefaultMailbox,
[|MessageFlag.Seen|])
client.Logout()
with
| ex ->
log.error "Failed to fetch new emails %A" ex
77
Async.Sleep(1000) |> Async.RunSynchronously
let t = new Thread(ThreadStart(imapEmailLoop))
t.Start()
log.info "Email bot started"
override this.get_link (token:string) =
setNull<string>()
module OperatorDialogRouting
open StackExchange.Redis
open System
open
open
open
open
open
open
Log
Operator
OperatorDialog
RedisUtility
Utility
RoutingAgentTypes
let log = NLog.LogManager.GetLogger("Agents.OperatorDialogRouting")
let RoutingDoNotRoute = "__DO_NOT_ROUTE__"
let RoutingSetTypeToString (setType:RoutingSetType) =
match setType with
| RoutingSetType.Language -> "lang"
| RoutingSetType.Customer -> "cust"
| RoutingSetType.Group -> "group"
| RoutingSetType.Skill -> "skill"
| RoutingSetType.CallCenter -> "callcenter"
| RoutingSetType.Operator -> "operator"
/// 1e8 - 1e11
let DialogMultiple : int64 = 1_00_000_000L
let GetOperatorScore (dialogCount: int) =
let start = new DateTime(2017, 1, 1)
let elapsed = new TimeSpan(DateTime.Now.Ticks - start.Ticks);
int64(dialogCount)*DialogMultiple + int64(elapsed.TotalMinutes)
let GetCurrentPriority() : int64 =
let start = new DateTime(2017, 1, 1)
let elapsed = new TimeSpan(DateTime.Now.Ticks - start.Ticks);
int64(elapsed.TotalMinutes)
[<AutoOpen>]
module Events =
let RoutingEnabledForOperatorEvent = Event<Operator>()
let OnRoutingEnabledForOperator = RoutingEnabledForOperatorEvent.Publish
let RoutingDisabledForOperatorEvent = Event<Operator>()
let OnRoutingDisabledForOperator = RoutingDisabledForOperatorEvent.Publish
type DialogDetachedEventArgs(dialog:Dialog.Dialog, operator:Operator,
dialogCount:int) =
inherit System.EventArgs()
member this.Dialog = dialog
member this.Operator = operator
member this.DialogCount = dialogCount
78
let DialogDetachedEvent = new Event<DialogDetachedEventArgs>()
let OnDialogDetached = DialogDetachedEvent.Publish
type DialogEventArgs(redis:IDatabase, operatorId:string, dialogId:string,
reason:string) =
inherit System.EventArgs()
member this.Redis = redis
member this.OperatorId = operatorId
member this.DialogId = dialogId
member this.Reason = reason
let DialogFinishEvent = new Event<DialogEventArgs>()
let OnDialogFinish = DialogFinishEvent.Publish
let DialogSuspendEvent = new Event<DialogEventArgs>()
let OnDialogSuspend = DialogSuspendEvent.Publish
let DialogResumeEvent = new Event<DialogEventArgs>()
let OnDialogResume = DialogResumeEvent.Publish
[<AutoOpen>]
module Storage =
// RoutingSet -> OperatorId[]
let RedisRoutingSet setType setName = RedisPrefix + (RoutingSetTypeToString
setType) +
"_set:" + setName
// Operator routing queue
let RedisOperatorRoutingQueue() = RedisPrefix + "ops_queue"
// Operator routing queue mutex
let RedisOperatorRoutingQueueMutex() = RedisPrefix + "MUTEX:ops_queue"
// Dialog processing queue key
let RedisDialogProcessingQueue() = RedisPrefix + "msg_queue"
// Dialog processing queue mutex key
let RedisDialogProcessingQueueMutex() = RedisPrefix + "MUTEX:msg_queue"
// Set of users operators
let RedisOmniUserIdOperators omniUserId = RedisPrefix + "users_operators:" +
omniUserId
let AddOperatorToRoutingSet (redis:IDatabase) (setType:RoutingSetType)
(setName:string) (operatorId:string) =
redis.SortedSetAdd(~~(RedisRoutingSet setType setName),
[|SortedSetEntry(~~operatorId, 0.0)|])
|> ignore
let RemoveOperatorFromRoutingSet (redis:IDatabase) (setType:RoutingSetType)
(setName:string) (operatorId:string) =
redis.SortedSetRemove(~~(RedisRoutingSet setType setName), ~~operatorId)
|> ignore
let IsOperatorInRoutingSet (redis:IDatabase) (setType:RoutingSetType)
(setName:string) (operatorId:string) : bool =
redis.SortedSetRank(~~(RedisRoutingSet setType setName),
~~operatorId).HasValue
let AddDialogToProcessingQueue (redis:IDatabase) (dialogId:string) =
log.debug "AddDialogToProcessingQueue %A" dialogId
redis.SortedSetAdd(~~RedisDialogProcessingQueue(),
[| SortedSetEntry(~~dialogId, float(DateTime.Now.Ticks)) |] ) |>
ignore
//
let RemoveDialogFromProcessingQueue (redis:IDatabase) (dialogId:string) =
log.debug "RemoveDialogFromProcessingQueue %A" dialogId
79
redis.SortedSetRemove(~~RedisDialogProcessingQueue(), ~~dialogId) |>
ignore
let PopDialogFromProcessingQueue (redis:IDatabase) : string option =
let dialogId = redis_sortedset_pop redis <| RedisDialogProcessingQueue()
dialogId |> StringToOption
let IsDialogInProcessingQueue (redis:IDatabase) (dialogId:string) =
redis.SortedSetRank(~~RedisDialogProcessingQueue(), ~~dialogId).HasValue
let AddOperatorToRoutingQueue (redis:IDatabase) (operatorId:string)
(score:int64) =
log.debug "AddOperatorToRoutingQueue %A" operatorId
redis.SortedSetAdd(~~RedisOperatorRoutingQueue(),
[| SortedSetEntry(~~operatorId, float(score)) |] )
|> ignore
let RemoveOperatorFromRoutingQueue (redis:IDatabase) (operatorId:string) =
log.debug "RemoveOperatorFromRoutingQueue %A" operatorId
redis.SortedSetRemove(~~RedisOperatorRoutingQueue(), ~~operatorId) |>
ignore
let IsOperatorInRoutingQueue (redis:IDatabase) (operatorId:string) =
redis.SortedSetRank(~~RedisOperatorRoutingQueue(), ~~operatorId).HasValue
let private GetRoutingSetList (operator:Operator) =
[
(RoutingSetType.Language, operator.languages);
(RoutingSetType.Customer, operator.customers);
(RoutingSetType.Group, operator.groups);
(RoutingSetType.Skill, operator.skills);
(RoutingSetType.Operator, [|operator.id|]);
(RoutingSetType.CallCenter, [|operator.callcenter|]);
]
///
///
///
///
///
let
<summary>
Adds operator to routing queue and all his routing sets
</summary>
<param name="redis"></param>
<param name="operator"></param>
EnableRoutingForOperator (redis:IDatabase) (operator:Operator) =
log.debug "EnableRoutingForOperator %A" operator.id
AddOperatorToRoutingQueue redis operator.id <| GetOperatorScore 0
for (setType, setNames) in GetRoutingSetList(operator) do
for setName in setNames do
AddOperatorToRoutingSet redis setType setName operator.id
RoutingEnabledForOperatorEvent.Trigger(operator)
///
///
///
///
///
let
<summary>
Removes operator from routing queue and all his routing sets
</summary>
<param name="redis"></param>
<param name="operator"></param>
DisableRoutingForOperator (redis:IDatabase) (operator:Operator) =
log.debug "DisableRoutingForOperator %A" operator.id
RemoveOperatorFromRoutingQueue redis operator.id
for (setType, setNames) in GetRoutingSetList(operator) do
for setName in setNames do
RemoveOperatorFromRoutingSet redis setType setName operator.id
RoutingDisabledForOperatorEvent.Trigger(operator)
/// <summary>
/// If operatorId and dialogId is not null and operator exists, then
/// RemoveDialogFromOperator,
80
/// AddOperatorToRoutingQueue
/// Return dialog to ProcessingQueue
/// SuspendDialog
/// </summary>
/// <param name="redis"></param>
/// <param name="dialogId"></param>
/// <param name="operatorId"></param>
/// <param name="update_ops_queue"></param>
/// <param name="return_to_msg_queue"></param>
/// <param name="suspend_dialog"></param>
let DetachDialogFromOperator (redis:IDatabase) (dialogId:string)
(operatorId:string) (update_ops_queue:bool)
(return_to_msg_queue:bool) (suspend_dialog:bool) =
log.debug "DetachDialogFromOperator %A %A" dialogId operatorId
if not(String.IsNullOrWhiteSpace(operatorId)) &&
not(String.IsNullOrWhiteSpace(dialogId)) then
RemoveDialogFromOperator redis operatorId dialogId
let dialogCount = GetOperatorDialogCount redis operatorId
if update_ops_queue then
AddOperatorToRoutingQueue redis operatorId (GetOperatorScore <|
int(dialogCount))
if suspend_dialog then
MediatorAPI.suspend_dialog dialogId "OPERATOR_DETACHED"
// Return dialog to processing queue
let dialog = MediatorAPI.get_dialog dialogId
if not <| isNull dialog then
if return_to_msg_queue then
AddDialogToProcessingQueue redis dialog.id
let operatorOpt = GetOperator redis operatorId
match operatorOpt with
| None -> ()
| Some operator ->
Events.DialogDetachedEvent.Trigger
<| new DialogDetachedEventArgs(dialog, operator, int(dialogCount))
let DetachAllDialogsFromOperator (redis:IDatabase) (operatorId:string) =
log.debug "DetachAllDialogsFromOperator %A" operatorId
let dialogIds = GetOperatorDialogIds redis operatorId
for dialogId in dialogIds do
DetachDialogFromOperator redis dialogId operatorId false true true
let FinishDialog (redis:IDatabase) (operatorId:string) (dialogId:string)
(reason:string) =
DetachDialogFromOperator redis dialogId operatorId true false false
MediatorAPI.finish_dialog dialogId reason
DialogFinishEvent.Trigger <| new DialogEventArgs(redis, operatorId,
dialogId, reason)
let SuspendDialog (redis:IDatabase) dialogId (reason:string) =
let operatorId = GetOperatorIdByDialogId redis dialogId
//RemoveDialogFromProcessingQueue redis dialogId
//DetachDialogFromOperator redis dialogId operatorId true false false
RemoveDialogFromOperator redis operatorId dialogId
let dialogCount = GetOperatorDialogCount redis operatorId
AddOperatorToRoutingQueue redis operatorId (GetOperatorScore <|
int(dialogCount))
MediatorAPI.suspend_dialog dialogId reason
DialogSuspendEvent.Trigger <| new DialogEventArgs(redis, operatorId,
dialogId, reason)
81
let ResumeDialog (redis:IDatabase) dialogId (operatorId:string) =
maybe {
let! operator = GetOperator redis operatorId
MediatorAPI.resume_dialog dialogId operator false
AddDialogToProcessingQueue redis dialogId
DialogResumeEvent.Trigger <| new DialogEventArgs(redis, operator.id,
dialogId, null)
} |> ignore
module OperatorDialogRoutingOperations
open StackExchange.Redis
open System
open
open
open
open
open
open
open
open
open
open
open
Agent
Bots
Log
Operator
OperatorDialog
OperatorDialogMessage
OperatorDialogRouting
OperatorState
Utility
RedisUtility.Lua
RoutingAgentTypes
let log = NLog.LogManager.GetLogger("Agents.OperatorDialogRoutingOperations")
[<AutoOpen>]
module Events =
let RoutingFoundEvent = new Event<_>()
let OnRoutingFound = RoutingFoundEvent.Publish
type public __RouteLuaParams() =
class
member val tempSetKey
: RedisKey = ~~"" with get, set
member val callCenterKey
: RedisKey = ~~"" with get, set
member val callCenterAny
: RedisKey = ~~"" with get, set
member val customerKey
: RedisKey = ~~"" with get, set
member val customerAny
: RedisKey = ~~"" with get, set
member val groupKey
: RedisKey = ~~"" with get, set
member val groupAny
: RedisKey = ~~"" with get, set
member val skillKey
: RedisKey = ~~"" with get, set
member val skillAny
: RedisKey = ~~"" with get, set
member val languageKey
: RedisKey = ~~"" with get, set
member val languageAny
: RedisKey = ~~"" with get, set
member val routingQueueKey : RedisKey = ~~"" with get, set
member val operatorKey
: RedisKey = ~~"" with get, set
member val omniUserIdOperatorsKey
: RedisKey = ~~"" with get, set
member val activeOperatorsKey : RedisKey = ~~"" with get, set
member val operatorId
: RedisValue = ~~"" with get, set
member val maxScore
: RedisValue = ~~"" with get, set
member val operatorScore
: RedisValue = ~~"" with get, set
member val currentPriority
: RedisValue = ~~"" with get, set
member val force
: RedisValue = ~~"" with get, set
end
let private
<@@
let
let
let
let
GetLuaCode() =
tmpCallCenterKey = &&"tempSetKey" + "_cc"
tmpCustomerKey = &&"tempSetKey" + "_cust"
tmpGroupKey = &&"tempSetKey" + "_group"
tmpSkillKey = &&"tempSetKey" + "_skill"
82
let tmpLangKey = &&"tempSetKey" + "_lang"
R.Execute("ZUNIONSTORE",
&&"callCenterAny"|]) |> ignore
R.Execute("ZUNIONSTORE",
&&"customerAny"|]) |> ignore
R.Execute("ZUNIONSTORE",
&&"groupAny"|]) |> ignore
R.Execute("ZUNIONSTORE",
&&"skillAny"|]) |> ignore
R.Execute("ZUNIONSTORE",
&&"languageAny"|]) |> ignore
[|tmpCallCenterKey; string 2; &&"callCenterKey";
[|tmpCustomerKey; string 2; &&"customerKey";
[|tmpGroupKey; string 2; &&"groupKey";
[|tmpSkillKey; string 2; &&"skillKey";
[|tmpLangKey; string 2; &&"languageKey";
R.Execute("ZINTERSTORE", [|&&"tempSetKey"; string 6; tmpCallCenterKey;
tmpCustomerKey;
tmpGroupKey; tmpSkillKey; tmpLangKey;
&&"routingQueueKey"|])
|> ignore
if &&"force" = "false" then
R.Execute("ZINTERSTORE", [|&&"tempSetKey"; string 2; &&"tempSetKey";
&&"activeOperatorsKey"|]) |> ignore
R.Execute("ZREMRANGEBYSCORE", [|&&"tempSetKey"; "(" + &&"maxScore";
"+inf"|]) |> ignore
// mutable чтобы упростить трансляцию в lua
let mutable opSetKey = ""
if &&"operatorKey" <> "" then
// Если у нас указан ключ оператора, то нужно пробовать роутить именно
на него
opSetKey <- &&"operatorKey"
else
// Иначе попробуем роутить на предыдущих операторов
opSetKey <- &&"omniUserIdOperatorsKey"
let setSize : int = ~~~R.Execute("ZCARD", [|opSetKey|])
if setSize > 0 then
let tempSetKey2 = &&"tempSetKey" + "_ops"
R.Execute("ZINTERSTORE", [|tempSetKey2; string 2; &&"tempSetKey";
opSetKey |])
|> ignore
let setSize : int = ~~~R.Execute("ZCARD", [|tempSetKey2|])
if setSize > 0 then
R.Execute("RENAME", [|tempSetKey2; &&"tempSetKey"|]) |> ignore
let ops : string[] = ~~~R.Execute("ZRANGEBYSCORE", [|&&"tempSetKey"; "inf"; "+inf"; "WITHSCORES";
"LIMIT"; string 0; string 1|])
R.Execute("DEL", [|&&"tempSetKey"|]) |> ignore
R.Execute("DEL", [|tmpCallCenterKey|]) |> ignore
R.Execute("DEL", [|tmpCustomerKey|]) |> ignore
R.Execute("DEL", [|tmpGroupKey|]) |> ignore
R.Execute("DEL", [|tmpSkillKey|]) |> ignore
R.Execute("DEL", [|tmpLangKey|]) |> ignore
if ops.Length > 0 then
let opScore : int = ~~~R.Execute("ZSCORE", [| &&"routingQueueKey";
ops.[0] |])
let score = (floor (float(opScore) / 100000000.0) + 1.0) * 100000000.0
+ float(&&"operatorScore")
R.Execute("ZADD", [|&&"routingQueueKey"; score.ToString(); ops.[0]|])
|> ignore
R.Execute("ZADD", [|&&"omniUserIdOperatorsKey"; string(int(&&"currentPriority")); ops.[0]|]) |> ignore
83
Return ops.[0]
else
Return null
@@> |> toLua
let private GetSuitableOperator (redis:IDatabase) (routingParams:RoutingParams) :
string option =
let operatorKey =
if not <| String.IsNullOrWhiteSpace routingParams.operatorId then
RedisRoutingSet RoutingSetType.Operator routingParams.operatorId
else
""
let luaCode = GetLuaCode()
let luaScript = LuaScript.Prepare(luaCode)
let routeLuaParams = new __RouteLuaParams()
routeLuaParams.tempSetKey <- ~~(Guid.NewGuid().ToString())
routeLuaParams.callCenterKey <- ~~(RedisRoutingSet RoutingSetType.CallCenter
routingParams.callcenter)
routeLuaParams.customerKey <- ~~(RedisRoutingSet RoutingSetType.Customer
routingParams.customer)
routeLuaParams.groupKey <- ~~(RedisRoutingSet RoutingSetType.Group
routingParams.group)
routeLuaParams.skillKey <- ~~(RedisRoutingSet RoutingSetType.Skill
routingParams.skill)
routeLuaParams.languageKey <- ~~(RedisRoutingSet RoutingSetType.Language
routingParams.language)
routeLuaParams.operatorKey <- ~~(operatorKey)
routeLuaParams.callCenterAny <- ~~(RedisRoutingSet RoutingSetType.CallCenter
"ANY")
routeLuaParams.customerAny <- ~~(RedisRoutingSet RoutingSetType.Customer
"ANY")
routeLuaParams.groupAny <- ~~(RedisRoutingSet RoutingSetType.Group "ANY")
routeLuaParams.skillAny <- ~~(RedisRoutingSet RoutingSetType.Skill "ANY")
routeLuaParams.languageAny <- ~~(RedisRoutingSet RoutingSetType.Language
"ANY")
routeLuaParams.routingQueueKey <- ~~RedisOperatorRoutingQueue()
routeLuaParams.operatorScore <- ~~(GetOperatorScore 0)
routeLuaParams.maxScore <- ~~(int64(routingParams.maxDialogs)*DialogMultiple 1000L)
routeLuaParams.operatorId <- ~~(routingParams.operatorId)
routeLuaParams.force <- ~~(if routingParams.force then "true" else "false")
routeLuaParams.omniUserIdOperatorsKey <- ~~(RedisOmniUserIdOperators
routingParams.omniUserId)
routeLuaParams.currentPriority <- ~~(GetCurrentPriority())
routeLuaParams.activeOperatorsKey <- ~~(RedisActiveOperators())
let op = (string)(luaScript.Evaluate(redis, routeLuaParams))
op |> StringToOption
let private GetCurrentlyRoutedOperatorId (redis:IDatabase) dialogId : string
option =
try
let op = GetOperatorIdByDialogId redis dialogId
if not(String.IsNullOrWhiteSpace(op)) && (IsOperatorOnline redis op) then
log.debug "Use current routing: %A, %A" op dialogId
// verify integrity
let dlgs = GetOperatorDialogIds redis op
if not(isNull dlgs) then
match Array.tryFind(fun id->id = dialogId) dlgs with
| Some _ -> Some op
| None -> None
else
84
None
else
None
with
|ex ->
log.error "GetCurrentlyRoutedOperatorId exception: %A" ex
None
let private RouteDialog (redis:IDatabase) (dialog:Dialog.Dialog) routingParams =
WithLock redis (RedisOperatorRoutingQueueMutex()) 300.0 (fun() ->
if String.IsNullOrWhiteSpace(routingParams.customer) then
//
log.debug "Unable to route - no customer (%A)"
routingParams.customer
None
else
let operatorId = maybe {
return! GetCurrentlyRoutedOperatorId redis dialog.id
return! GetSuitableOperator redis routingParams
}
match operatorId with
| Some operatorId ->
// If it's the same operator, then do nothing.
AddDialogToOperator redis operatorId dialog.id
log.debug "Routing found: operator=%A dialog_id=%A" operatorId
dialog.id
//
Some operatorId
| None ->
log.debug "No route nor operator found, dialog_id=%A" dialog.id
None
) (fun (ex) ->
log.warn "Unable to acquire lock for %A" RedisOperatorRoutingQueueMutex
match ex with
| null -> ()
| ex ->
log.error "RouteDialog: Exception %A" ex
None
)
let GetDialogRouting (redis:IDatabase) (dialog:Dialog.Dialog) =
//async { log.debug "GetDialogRouting %A" dialog } |> Async.StartAsTask |>
ignore
let max_dialogs = get_context_value dialog.context "routing:max_dialogs"
let routingParams = {
callcenter = dialog.routing.callcenter;
customer = dialog.customer_id;
group = dialog.routing.group
language = dialog.routing.lang
skill = dialog.routing.skill
maxDialogs = (if String.IsNullOrWhiteSpace(max_dialogs) then 10 else
Int32.Parse(max_dialogs))
omniUserId = dialog.omni_user_id
operatorId = if dialog.routing.force then dialog.routing.operator_id else
null
force = dialog.routing.force
}
let op = RouteDialog redis dialog routingParams
match op with
| Some op ->
let dialog =
if not <| isNull dialog.stat && dialog.stat.routed = 0L then
85
{dialog with
stat = {dialog.stat with routed = unixtime_msec_now()}
}
else
dialog
let dialog =
if not <| isNull dialog.cur_session && not <| isNull
dialog.cur_session.stat && dialog.cur_session.stat.routed = 0L then
{dialog with
cur_session = {dialog.cur_session with stat =
{dialog.cur_session.stat with routed = unixtime_msec_now()}}
}
else
dialog
MediatorAPI.update_dialog {dialog with routing = {dialog.routing with
operator_id = op; force = false}}
{dialog.routing with operator_id = op; force = false}
| None ->
//log.debug "Put dialog to queue to process later"
AddDialogToProcessingQueue redis dialog.id
{dialog.routing with operator_id=null}
let! dialogId, dialog, routing =
WithLock redis (RedisDialogProcessingQueueMutex()) 300.0 (fun() -> maybe {
let! dialogId = PopDialogFromProcessingQueue redis
try
let! dialog = MediatorAPI.get_dialog dialogId |> RefToOption
th.Tick("get dialog")
let dialog = dialog :?> Dialog.Dialog
let routing = GetDialogRouting redis dialog
th.Tick("get dialog routing")
return (dialogId, dialog, routing)
with
| ex ->
log.error "TryProcessDialogRoutingQueue Exception: %A" ex
AddDialogToProcessingQueue redis dialogId
return! None
}) (fun(ex) ->
log.error "TryProcessDialogRoutingQueue Exception: %A" ex
None
)
let! operatorId = routing.operator_id |> StringToOption
let! operator = GetOperator redis routing.operator_id
th.Tick("Get Operator")
async {
let dialogCount = GetOperatorDialogCount redis operatorId
RoutingFoundEvent.Trigger(operator, dialogId, dialogCount)
} |> Async.StartAsTask |> ignore
SendCachedDialogMessages redis dialog routing |> Async.StartAsTask |> ignore
th.Tick("Send cached dialog messages")
th.Total()
return dialogId
}
let RerouteDialog (redis:IDatabase) (dialogId:string) (routing:Routing) =
let res =
maybe {
let! operatorId = GetOperatorIdByDialogId redis dialogId |>
86
StringToOption
let! dialog = MediatorAPI.get_dialog_opt dialogId
let newDialog =
{dialog with
routing = routing
}
MediatorAPI.update_dialog newDialog
DetachDialogFromOperator redis dialogId operatorId true true false
}
match res with
| Some _ -> true
| None -> false
let call_agent (redis : IDatabase) (customer:Customer) (agent:Agent)
(msg:Message) =
async{
let rec call_agent_rec (customer:Customer) (agent:Agent) (msg:Message)
(cnt:int) =
async {
let th = new TimingHelper(true, "CallAgent")
th.Start()
let failover() =
async{
if not(String.IsNullOrWhiteSpace agent.failover_agent) &&
cnt<20 then
log.Debug "Call failed, do failover"
let failover_agent = get_agent customer
agent.failover_agent
let! reply = call_agent_rec customer failover_agent
msg (cnt+1)
return reply
else
log.Debug("Call failed and no failover agent found or
too many failovers ({0})", cnt)
return setNull<ReplyMessage>()
}
log.Debug("call agent {0} {1}", agent.id, msg.id)
try
let msg = {msg with agent_cust_id = agent.cust_agent_id;
agent_id = agent.id}
try
th.Tick("process message preparation")
let! reply = send_message_async agent.client_webhook msg
agent.parameters
th.Tick("send_message to agent")
log.Debug("Received reply from agent: {0}", JsonSerialize
reply)
let! reply =
async {
if not(isNull reply) then
return { reply with context =
(override_context (if isNull msg then [||] else msg.context) reply.context) }
else
return! failover()
}
th.Tick("reply processing")
return reply
with
|ex as Exception ->
87
log.error "BotMediator.call_agent failed on agent call %A
%A" agent ex
return! failover()
with
|ex as Exception ->
do! MediatorStorage.delay_message redis msg
log.error "BotMediator.call_agent %A exception: %A %A" agent
msg ex
return setNull<ReplyMessage>()
}
return! call_agent_rec customer agent msg 0
}
let rec process_agent_reply_rec
(redis : IDatabase)
(orig_msg:Bots.Message)
(agent:Agent)
(msg:Bots.Message)
(reply:Bots.ReplyMessage)
(cnt:int)
(customer:Customer)
(dialog:Dialog) = async {
let th = new TimingHelper(true, "ProcessAgentReplyRec")
th.Start()
try
let! next_agent, text, dialog =
match detect_command reply.message.text with
| Left (command, text) -> async {
let command_result = run_command command customer agent dialog
orig_msg text
let dialog = {dialog with default_agent =
command_result.default_agent }
do! MediatorStorage_Dialogs.update_dialog_async redis dialog
return command_result.next_agent, command_result.message_text,
dialog
}
| Right text ->
async { return (if isNull agent then null else agent.next_agent),
text, dialog }
th.Tick("run_command")
if not <| isNull next_agent then
// do redirect
let agent = get_agent customer next_agent
th.Tick("get agent")
if not <| isNull agent then
let msg =
if not(isNull(msg)) then
{msg with
message_type = reply.message_type
message = {msg.message with text = text};
agent_cust_id = agent.cust_agent_id;
routing = reply.routing;
context = (override_context msg.context (if isNull
reply then [||] else reply.context))
slot_context = reply.slot_context
}
else
let emp_msg = Bots.empty_message()
{ emp_msg with
message_type = reply.message_type
channel = { emp_msg.channel with customer_id =
reply.customer_id; };
user =
88
{ emp_msg.user with session_id = reply.session_id;
omni_user_id = reply.omni_user_id }
message = { emp_msg.message with text = text; }
routing = reply.routing;
context = reply.context
slot_context = reply.slot_context
}
th.Tick("construct message")
match CanSendMessageType agent msg.message_type with
| true ->
if cnt < 20 then
let! reply = call_agent redis customer agent msg
if not <| isNull reply then
do! process_agent_reply_rec redis orig_msg agent msg
reply (cnt+1) customer dialog
else
log.error "Too many redirects detected - processing of
this message has been stopped"
| false ->
log.debug "Unsupported message type for agent: %A. Ignoring"
msg.message_type
else
log.error "Agent %A for customer %A not found!" next_agent
customer
else
let omni_user_id =
if isNull dialog then
reply.omni_user_id
else
dialog.omni_user_id
let user = get_user_by_omni_user_id redis omni_user_id
let context = customer.default_context
let request_key = "mediator:request"
let force_key = "force"
th.Tick("load user")
let all_request_types, request_types_with_params =
context
|> Array.filter (fun elem -> elem.Length > 1 &&
elem.[0].Contains(request_key))
|> Array.partition (fun elem -> String.Compare(elem.[0],
request_key) = 0)
th.Tick("get request types")
let reply_msg_request =
if not(Array.isEmpty all_request_types) && not(Array.isEmpty
request_types_with_params) ||
not(Array.isEmpty all_request_types) && Array.isEmpty
request_types_with_params then
let all_request_types = all_request_types.[0].[1..]
let request_types_with_params =
if Array.isEmpty request_types_with_params then [||]
else request_types_with_params |> Array.map
prepare_request_types_with_params
let reply_msg_request =
get_reply_msg_request all_request_types
request_types_with_params user force_key
|> Array.filter (fun elem -> not(Array.isEmpty elem))
reply_msg_request
89
elif Array.isEmpty all_request_types && not(Array.isEmpty
request_types_with_params) then
raise (ContextError "Bad context!")
[||]
else
[||]
th.Tick("get reply message request")
log.debug "no redirect - send reply to a user"
let reply =
{reply with
id = Guid.NewGuid().ToString()
message =
{reply.message with
text = text
request = reply_msg_request
}
context = if isNull dialog then [||] else dialog.context
}
th.Tick("construct reply")
if not <| ReplyMessageIsEmpty reply then
let reply = { reply with id = Bots.generate_id() }
let fname, lname =
if isNull dialog then
null, null
else
dialog.fname, dialog.lname
do! reply_message redis reply customer fname lname
th.Tick("send reply")
if not <| isNull dialog then
let context = override_context dialog.context reply.context
do! MediatorStorage_Dialogs.update_dialog_async
redis
{dialog with
context = context
slot_context = reply.slot_context
}
th.Tick("Update dialog")
th.Total()
with
|ex as Exception ->
log.error "BotMediator.process_reply_message exception: %A %A" msg ex
do! MediatorStorage.delay_message redis msg
}
let process_message
(redis:IDatabase)
(msg:Bots.Message)
(fileservice_host:string)
channel_transformations
transformation_webhooks = async {
try
let th = new TimingHelper(msg.message_type=MessageType.Message,
"ProcessMessage")
th.Start()
log.Debug("Process_message {0}", msg.id)
let! omniUserId = MediatorStorage.GetOmniByChannel redis
msg.channel.channel_id msg.user.channel_user_id
90
let! dialogId = getDialogId redis omniUserId msg.channel.customer_id
if msg.message_type = MessageType.Message && MessageIsEmpty msg then
log.Warn("Ignored empty message {0}", JsonSerialize msg)
()
elif msg.message_type = MessageType.UpdateDialogScore &&
String.IsNullOrWhiteSpace dialogId then
do! UpdateDialogScore msg.message.text omniUserId
msg.channel.customer_id
elif msg.message_type = MessageType.ReadConfirmation && isNull dialogId
then
()
else
let! msg = apply_transformations msg channel_transformations
transformation_webhooks
th.Tick("apply_transformations")
let customer = get_customer msg.channel.customer_id
th.Tick("get_customer")
let! msg = prepare_message redis msg fileservice_host customer
th.Tick("prepare_message")
let! dialog = MediatorStorage_Dialogs.get_dialog redis
msg.user.session_id
th.Tick("get_dialog")
// Send confirmation to channel
if msg.message_type = MessageType.Message && not <| isNull
msg.id_from_channel then
let confirmationSignal =
{ get_reply_msg msg (setNull<Attachment>()) "" with
message_type = MessageType.ReceivedByMediator
parent_message_id = msg.id_from_channel
}
let customer = get_customer msg.channel.customer_id
do! reply_message redis confirmationSignal customer "" ""
elif msg.message_type = MessageType.UpdateDialogScore ||
msg.message_type = MessageType.CloseDialogIntention then
do! UpdateDialogScore msg.message.text msg.user.omni_user_id
msg.channel.customer_id
elif msg.message_type = MessageType.FinishDialog then
do! FinishDialog
msg.message.text
msg.user.session_id
msg.user.omni_user_id
msg.channel.customer_id
th.Tick("send_confirmation")
let agent = get_agent customer dialog.default_agent
// default
agent
th.Tick("get_agent")
log.Debug("Default agent: {0}, url: {1}", dialog.default_agent,
agent.client_webhook)
match CanSendMessageType agent msg.message_type with
| true ->
try
if not <| String.IsNullOrWhiteSpace
MediatorConfig.config.timerservice_addr &&
msg.message_type = MessageType.Message then
let timerservice_addr =
MediatorConfig.config.timerservice_addr
91
http_post_no_body(timerservice_addr +
"/on_user_reply?dialog_id=" + dialog.id)
|> ignore
with
| ex ->
log.warn "Failed to send event to TimerService: %A" ex
let! reply = call_agent redis customer agent msg
th.Tick("call_agent")
do! process_agent_reply redis (Some dialog) agent msg reply
th.Tick("process_agent_reply")
| false -> ()
th.Total()
with
|ex as Exception ->
do! MediatorStorage.delay_message redis msg
log.error "BotMediator.process_message exception: %A %A" msg ex
}
1 !!
�I J,
!t
8
�
92
МИНИСТЕР СТВО ОБРАЗОВЛНИЯ
И НАУКИ РОССИЙСКОЙ
ФЕДЕРАЦИИ
ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕI
ШОЕ БЮДЖЕТНОЕ
ОБРАЗОВАТЕЛЬН(2Е УЧРЕЖДЕНИЕ
ВЫСШЕГО ОБРАЗОВАНИЯ
«ОРЛОВСКИИ ГОСУДАРСТВЕIП-IЫЙ
РСИТЕТ
:m.✓.IЕНИ И.С. ТУРГЕНЕВАУНИВЕ
»
УДОСТОВЕРЯЮЩИЙ ЛИ СТ № 140192
К ВЫПУСКНОЙ КВАЛИФИКАЦИОIШОЙ РАБОТЕ
на демонстрационный материал, представленный в электронном виде
Студента Барбашова Даниила Владиславовича
шифр 140192
Институт приборостроения, автоматизации и информационных технологии
Кафедра информационных систем
09.03.04 Программная инженерия
Промышленная разработка программного обеспечения
Наименование документа: Демонстрационные плакаты к выпускной
квалификационной работе
Документ разработал:
Студент
Барбашо в Д.В.
Документ согласован:
Руково ди тель
Нормоконтроль
· Документ утвержден:
Зав. кафедрой
Фролов А.И.
Ю
Ужаринский А. .
Ф ролов А.И.
Орел 2018
8
93
ИНФОРМАЦИОННО-ПОИСКОВАЯ ХАРАКТЕРИСТИКА
ДОКУМЕНТА НА ЭЛЕКТРОННОМ НОСИТЕЛЕ
Наименование
группы атрибутов
атрибута
1. Описание
Обозначение документа
документа
(идентификатор(ы)
файла(ов))
Наименование документа
2. Даты и время
3. Создатели
4. Внешние
ссылки
5. Защита
6. Характеристики
содержания
Характеристики документа
на электронном носителе
\Презентация.pptx
Демонстрационные плакаты
к выпускной
квалификационной работе
Класс документа
ЕСКД
Вид документа
Оригинал документа на
электронном носителе
Аннотация
Демонстрационный
материал, отображающий
основные этапы выполнения
выпускной
квалификационной работы
Использование документа Операционная система
Windows 10, Microsoft
PowerPoint 2016
Дата и время
18.06.2018
копирования документа
Дата создания документа 29.05.2018
Дата утверждения
18.06.2018
документа
Автор
Барбашов Д.В.
Изготовитель
Барбашов Д.В.
Ссылки на другие
Удостоверяющий лист
документы
№ 140192
Санкционирование
ОГУ имени И.С. Тургенева
Классификация защиты
По законодательству РФ
Объем информации
413696 Б
документа
94
7. Структура
документа(ов)
Наименование плаката
(слайда) №1
Наименование плаката
(слайда) №2
Наименование плаката
(слайда) №3
Наименование плаката
(слайда) №4
Наименование плаката
(слайда) №5
Наименование плаката
(слайда) №6
Наименование плаката
(слайда) №7
Наименование плаката
(слайда) №8
Наименование плаката
(слайда) №9
Наименование плаката
(слайда) №10
Титульный лист
Рост популярности текстовых
каналов связи
и текущие решения в
контакт-центрах
Взаимодействие клиентов и
операторов
через каналы
Функциональная модель
системы
Общая архитектура системы
Основные классы системы
Взаимодействие компонентов
системы при обработке
входящих сообщений
Алгоритмы подсистемы
маршрутизации
Алгоритм диспетчеризации
Технологии и особенности
реализации
АНТИПЛАГИАТ
ТВОРИТЕ СОБСТВЕННЫМ УМ
Орловский государственный
университет имени И.С. Тургенева
ОМ
СПРАВКА
о результатах проверки текстового док
умента
на наличие заимствований
Проверка выполнена в системе
Антиплагиат.ВУЗ
Автор работы
Барбаwов Даниил Владиславович
Факультет, кафедра,
номер группы
ИПАИТ
Тип работы
Не указано
Название работы
Барбашов Даниил Владиславович VKR 090304 Barbashov D V 2018.docx
Название файла
VKR 090304 Barbashov D V 2018.docx
Процент заимствования
2,62%
Процент цитирования
0,38%
Процент оригинальности
97,01%
Дата проверки
12:42:52 18 июня 2018r.
Модули поиска
Сводная коллекция ЭБС; Коллекция РГБ; Цитирование; Коллекция eLIBRARY.RU;
Модуль поиска Интернет; Модуль поиска перефразирований eL\BRARY.RU; Модуль
поиска перефразирований Интернет; Модуль поиска общеупотребительных
выражений; Модуль поиска "ФГБОУ ВО ОГУ им. И.С.Тургенева"; Кольцо вузов
Работу проверил
Ужаринский Антон Юрьевич
ФИО
]
проверяющего
Дата подписи
Чтобы убедиться
в подлинности справки,
используйте QR-код, который
содержит ссылку на отчет.
уженное заимствование
ответ на вопрос, является ли обнарусмотрени
е проверяющего.
на
ляет
остав
ма
систе
м,
ектны
корр
ол ованию
Предоставленная информац ия не подлежит исп ьз
в коммерческих целях.
1/--страниц
Пожаловаться на содержимое документа