Сценарий бота для голосовой рассылки
В голосовых рассылках используются синтез и распознавание речи.
Синтез речи — процесс генерирования речи по печатному тексту.
Распознавание речи — процесс преобразования речи в текст.
В связи с этим написание сценария бота имеет свои особенности. В этой статье рассмотрим:
- адаптацию сценария для телефонного канала;
- настройку голосового синтеза;
- использование аудиофайлов;
- работу со списком номеров;
- планирование звонка из сценария;
- проставление результатов обзвона;
- расширение отчета по рассылке;
- получение записей звонков.
Адаптация сценария для телефонного канала
Распознавание
При адаптации сценария бота для телефонного канала следует отлавливать и обрабатывать событие speechNotRecognized. Событие возникает, если в течение 5 секунд запрос клиента невозможно распознать или на распознавание ничего не пришло.
Например:
- Бот отлавливает событие и просит клиента повторить фразу ещё раз:
state: speechNotRecognized || noContext = true
event: speechNotRecognized
a: Повторите, пожалуйста, Вас плохо слышно.
go!: {{$session.lastState}}- Выбирается одна из фраз:
state: CatchAll
event!: speechNotRecognized
q!: *
random:
a: Не могли бы вы повторить?
a: Я не расслышала.
a: Повторите, пожалуйста.
a: Извините. я не смогла расслышать ваш ответ.Автоответчик
При попадании бота на автоответчик, расходуются минуты телефонии.
Так как при попадании бота на автоответчик, расходуются минуты телефонии, предусматривайте это в сценарии. Например, бот просит клиента повторить реплику, если в ответ тишина или не удалось распознать фразу. После пяти попыток бот завершит звонок.
state: NoInput || noContext=true
event: speechNotRecognized
script:
$session.noInputCounter = $session.noInputCounter || 0;
$session.noInputCounter++;
if: $session.noInputCounter >= 5
a: Кажется какие-то проблемы со связью. Перезвоню позднее.
script:
$dialer.hangUp('(Бот повесил трубку)');
else:
a: Вас плохо слышно, повторите, пожалуйста!Другие event для телефонного канала
Завершение вызова ботом
Бот может сам завершать вызов с помощью метода $dialer.hangUp('(Бот повесил трубку)').
Фраза Бот повесил трубку будет отображаться в не голосовом канале, который вы используете для отладки бота.
Например:
state: HangUp
a: Всего доброго. До свидания.
script:
$dialer.hangUp('(Бот повесил трубку)');Аргумент для $dialer.hangUp() можно не передавать.
Используйте event: botHangup, чтобы отследить событие, когда разговор окончен по запросу бота.
Например:
state: BotHangUp
event: botHangup
script:
log("Got event botHangup");Завершение вызова клиентом
Используйте event: hangup, чтобы отследить событие, когда клиент завершил вызов.
Например, чтобы определять в каком стейте находился клиент, перед тем как был завершен вызов:
state: ClientHungUp
event!: hangup
script:
getClientHangUpReaction($session.lastState);При этом, если бот сам завершает вызов, событие hangup не возникает.
Вы можете определить прерывание фразы бота клиентом в сценарии. При этом фраза бота будет прервана сразу при появлении речи клиента.
Тоновый режим
Бот также может запрашивать и обрабатывать отправку dtmf-сообщения (цифры/символы в тоновом режиме).
Запрос dtmf-сообщения:
script:
$response.replies = $response.replies || [];
$response.replies.push({
type: 'dtmf',
max: 4, //максимальное количество цифр, которое ожидается от абонента.
timeout: 15000 //интервал ожидания ввода от абонента в миллисекундах, число.
});Далее отлавливается запрос, например, с цифрами:
state: Digits
q: $regexp<\d+>
a: вы набрали {{$parseTree.text}}!Событие noDtmfAnswerEvent возникает, если абонент не ввел dtmf-сообщение:
state: noDTMF || noContext = true
event: noDtmfAnswerEvent
a: Вы ничего не выбрали.
go!: {{$session.lastState}} Сессии
Подробнее о сессиях в платформе JAICP
В голосовых рассылках сессия стартует в момент, когда клиент поднял трубку либо сам позвонил. Окончание диалога считается завершением сессии.
При этом, если вы используете этот же сценарий для других каналов, сессия для них будет стартовать в момент первого обращения клиента, когда для него нет другой активной сессии.
Не рекомендуется для голосовых рассылок искусственно разбивать сессии с использованием реакции newSession.
Настройка голосового синтеза
При оформлении реплик бота вы можете при помощи разметки тоньше управлять синтезируемыми звуками.
- При необходимости ударные гласные в слове отмечайте символом
+, например:
a: Это система автоматического обзв+она.- Длинные слова можно разбить на короткие и проставлять ударения для каждого из них, например:
a: Мн+ого пр+офильный проект.-
Некоторые слова можно попробовать писать так, как они слышатся. Например,
«пожалуйста» — пож+алуста. -
Каждый отделенный пробелом пунктуационный знак преобразуется в паузу длительностью 50-100 мс. Таким образом можно задавать небольшие паузы последовательностью дефисов. Например:
a: Приветствуем {{$dialer.getPayload().name}}! Ответьте на вопрос: - - - - да, нет или наверноеНе стоит создавать таким образом большие паузы. Длинная последовательность дефисов может привести к звуковым артефактам при синтезе.
Использование аудиофайлов
Вы можете использовать в качестве ответа бота предзаписанный аудиофайл. Платформа позволяет использовать аудио в формате PCM, формат файла .wav. Настройки PCM:
- постоянный битрейт
128 кб/c; - 1 канал (моно);
- частота дискретизации
8000Hz; - кодирование
16-bit(PCM) little endian.
В реплику бота добавьте тег audio со ссылкой на аудиофайл с расширением .wav.
Например:
state: audio
audio: https://example.wav // ссылка на аудиофайл (любое хранилище)Работа со списком номеров
При создании списка номеров для голосовой рассылки вы можете добавить в список дополнительные поля. Например Имя, Фамилия, Город. Валидация таблицы будет проводиться только по первому столбцу с номерами, остальные будут загружены как текстовые поля.
Вы можете использовать информацию из дополнительных полей в сценарии при рассылке.
Метод $dialer.getPayload() возвращает json-объект с заполненными полями списка номеров.
Например, мы загружаем список номеров:
| phone | name | address |
|---|---|---|
| 79990000000ᅠᅠ | Иван ᅠᅠ ᅠᅠ | Москва |
Вызов метода $dialer.getPayload() вернет json-объект:
{
"phone": "79990000000",
"name": "Иван",
"address": "Москва"
}Использование в сценарии:
state: Hello
a: Привет!
if: $dialer.getPayload().name
a: Вас зовут {{$dialer.getPayload().name}}
else:
a: Я не знаю вашего имени.Если список загружен без заголовков, то вызов метода $dialer.getPayload() вернет json-объект:
{0: "79990000000", 1: "Иван", 2: "Москва"}В таком случае var name = $dialer.getPayload()[1].
Если значение в колонке пустое, то при вызове функции $dialer.getPayload().address будет возвращено пустое значение.
{
"phone": "79990000000",
"name": "Иван",
"address": ""
}Метод $dialer.getCaller() возвращает строку с телефоном пользователя в том виде, в котором его присылает телефонная станция.
Использование в сценарии:
state: Hello
a: Привет!
a: Ваш номер {{getCaller()}}.
if: $dialer.getPayload().name
a: Вас зовут {{$dialer.getPayload().name}}
else:
a: Я не знаю вашего имени.Планирование звонка из сценария
Во время разговора бота с клиентами может потребоваться запланировать новый звонок. Например, когда клиент просит перезвонить попозже.
При помощи метода $dialer.redial() вы можете запланировать новый звонок и переопределить политику звонков на данный номер из сценария.
Параметры метода соответствуют параметрам CallJobParametersList Calls API.
| Параметр | Пример | Описание |
|---|---|---|
startDateTime |
2020-03-23T00:00:00Z |
Начальная дата звонка. Принимает объект Date. Звонок будет совершен в интервале [startDateTime, finishDateTime]. |
finishDateTime |
2020-03-23T00:00:00Z |
Конечная дата звонка. Принимает объект Date. Звонки после finishDateTime совершаться не будут. |
allowedTime |
"mon": [{"localTimeFrom": "10:00", "localTimeTo": "11:30"}] |
Для каждого дня недели может быть указан один или несколько рекомендованных интервалов для звонков. localTimeFrom — самое раннее время для звонка. Время не абсолютное, для звонка будет учитываться локальное время абонента. localTimeTo — самое позднее время для звонка. Время не абсолютное, для звонка будет учитываться локальное время абонента. |
allowedDays |
["sun"] |
Разрешенные дни недели для звонка. |
maxAttempts |
1 |
Количество попыток дозвониться. |
retryIntervalInMinutes |
120 |
Пауза между попытками дозвониться, указывается в минутах. |
gmtZone |
"+03:00" |
Часовой пояс. Принимает значения от "-18:00" до "+18:00" или согласно ID часовых поясов, установленных IANA TZBD. |
Обратите внимание, что метод $dialer.redial() не используется без параметров. Для корректной работы метода необходимо обязательно задать параметр startDateTime или allowedTime. Если остальные параметры не будут указаны, то значения будут взяты из настроек голосовой рассылки.
Например, задаем параметры для повторного звонка:
state:
q!: Сall back
script:
$dialer.redial({
allowedTime: {
"mon": [{"localTimeFrom": "10:00", "localTimeTo": "11:30"},
{"localTimeFrom": "13:00", "localTimeTo": "14:30"}],
"fri": [{"localTimeFrom": "12:30", "localTimeTo": "15:00"}],
"default": [{"localTimeFrom": "10:00", "localTimeTo": "18:00"}]
},
retryIntervalInMinutes: 120,
maxAttempts: 3
})
a: okДля рекомендуемого интервала должны быть указаны обе границы localTimeFrom и localTimeTo. Если одна из границ не указана, то возвращается ошибка.
В данном примере повторный звонок будет совершен в понедельник в промежутках с 10:00 до 11:30 и с 13:00 до 14:30, в пятницу с 12:30 до 15:00. Рекомендации для других не явно указанных дней задаются в параметре allowedTime с ключом default.
Если в запросе указывается параметр allowedDays, то вызов рассылки может состояться только в тот день, который явно указан.
state:
q!: Сall back
script:
$dialer.redial({
allowedDays: ["sat", "sun"],
allowedTime: {
"fri": [{"localTimeFrom": "12:30", "localTimeTo": "15:00"}],
"default": [{"localTimeFrom": "10:00", "localTimeTo": "18:00"}]
},
retryIntervalInMinutes: 120,
maxAttempts: 3
})
a: okВ этом примере, несмотря на то, что для пятницы интервал указан явно, вызов может состояться только в субботу или в воскресенье.
Обратите внимание, что рекомендуемые интервалы внутри одного дня должны быть указаны без пересечений.
Примеры пересечений:
"mon": [{"localTimeFrom": "10:00", "localTimeTo": "13:30"},
{"localTimeFrom": "13:00", "localTimeTo": "14:30"}],
"tue": [{"localTimeFrom": "10:00", "localTimeTo": "18:30"},
{"localTimeFrom": "13:00", "localTimeTo": "14:30"}],Если в заданном интервале параметр localTimeFrom больше, чем localTimeTo, то считается, что интервал указан корректно. Он начинается в указанный день, а завершается на следующий день. Повторные звонки в следующем примере будут проходитть с 8 часов вечера в понедельник до 3 ночи во вторник:
"mon": [{"localTimeFrom": "20:00", "localTimeTo": "03:00"}],Задаем параметры для повторного звонка из реплики клиента. Для этого используем заполненый слот $parseTree._Date.timestamp для определения начальной даты звонка:
state: Перезвоните
intent!: /Перезвоните
a: Ок, перезвоню!
script:
$dialer.redial({startDateTime: new Date($parseTree._Date.timestamp)})
$dialer.hangUp();Задаем параметры для повторного звонка по просьбе клиента с дополнительными параметрами:
state:
q!: перезвоните через час
script:
var redialTimeFrom = moment().add(60, "m").toDate();
var redialTimeTo = moment().add(75, "m").toDate();
$dialer.redial({
startDateTime: redialTimeFrom, finishDateTime: redialTimeTo, // будем звонить в интервал [now + 1h, now + 1h 15m]
localTimeFrom: "00:00", localTimeTo: "00:00", // игнорируем ограничения на время звонков по времени абонента
maxAttempts: 2, // сделаем 2 попытки дозвониться
retryIntervalInMinutes: 5 // с паузой 5 минут между попытками
})
a: хорошо, я перезвоню вам через часВ рамках одного диалога можно запланировать только один звонок. Например, клиент просит перезвонить бота. При обработке последующих реплик клиента вызовы $dialer.redial() игнорируются, так как параметры для повторного звонка были заданы ранее.
Обратите внимание, что параметры $dialer.redial() могут быть переопределены, если не последовало ответа бота. Например, в postProcess или в другом стейте, если был совершен переход по go! без ответа бота.
Для одной голосовой рассылки из сценария можно задать максимум 5 повторных звонков для каждого номера.
Подробнее о приоритете выполнения и политике звонков
Проставление результатов обзвона
Проставляя результаты обзвона в сценарии, вы добавляете пользовательские метрики в статистику.
Метод $dialer.setCallResult позволяет проставлять результат обзвона для более развернутой статистики.
Например, рассмотрим сценарий:
state: Yes
q: Да
a: Спасибо за доверие! До свидания!
script:
$dialer.setCallResult("YES");
$dialer.hangUp();
state: No
q: Нет
a: Очень жаль, что вы отказались. До свидания!
script:
$dialer.setCallResult("NO");
$dialer.hangUp();
state: Maybe
q: Наверное
a: Мы вам перезвоним попозже. До связи!
script:
$dialer.setCallResult("MAYBE");
$dialer.hangUp();Теперь в таблице с метриками при попадании в определенный стейт будут отображаться результаты обзвона: YES, NO, MAYBE.
Если в сценарии не требуются расширенные метрики, используйте методы, проставляющие результаты о принятии или отклонении предложения, но при этом не требующие заполнения названия.
Методы $dialer.setCallResultAccepted и $dialer.setCallResultRejected позволяют проставлять результаты как ACCEPTED и REJECTED соответственно.
Сценарий:
state: Yes
q: Да
a: Спасибо за доверие! До свидания!
script:
$dialer.setCallResultAccepted();
$dialer.hangUp();
state: No
q: Нет
a: Очень жаль, что вы отказались. До свидания!
script:
$dialer.setCallResultRejected();
$dialer.hangUp();В таблице с метриками при попадании в определенный стейт будут отображаться результаты обзвона ACCEPTED и REJECTED.
Расширение отчета по рассылке
Метод $dialer.reportData($header, $value, $order) позволяет добавить колонки с произвольными данными в отчет .xls по рассылке.
Параметры метода:
$header(string) — название колонки в отчете;$value(string) — значение;$order(number) — порядок сортировки колонок в выгружаемом отчете.xls.
Разработчик сценария может добавить колонки в отчет, которые требуются для определенной рассылки, например:
- Для получения данных в ходе звонка:
$dialer.reportData("ФИО", "Иванов Иван")— в отчете по результатам обзвона будет добавлена колонкаФИОсо значениемИванов Иван.
- Для фиксирования фразы и факта прохождения по этапу:
$dialer.reportData("Этап_Отказ", "Нет, не нужно")— в отчете добавится колонка с названием этапаЭтап_Отказ, содержащая формулировку отказаНет, не нужно.
Параметр $order указывать необязательно, при этом по умолчанию передается значение 0. Колонки в отчете сортируются по убыванию значений $order, а при их совпадении — по алфавиту.
Рассмотрим сценарий:
theme: /
state:
q!: *start
a: Привет! Как тебя зовут?
state: Name
q: *
a: Здравствуй, {{$parseTree.text}}!
script:
$dialer.reportData("Имя", $parseTree.text);
a: Как ты относишься к курению?
state: Smoking
q: *
a: Так и запишем: "{{$parseTree.text}}".
script:
$dialer.reportData("Отношение к курению", $parseTree.text, 1);Здесь колонка Отношение к курению будет первой среди пользовательских колонок, так как явно указан $order, равный 1. Колонка Имя будет второй, так как здесь $order не передан и равен 0.
В сценарии с большим числом колонок в зависимости от значений $order они будут расположены так:
| Пол | Имя | Отчество | Фамилия | Отношение к курению |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | -1 |
Обратите внимание, что количество колонок в отчете по рассылке ограничено. Обратитесь к администратору аккаунта за подробной информацией.
Получение записей звонков
Если для телефонного канала включен параметр Записывать звонки, вы можете формировать в сценарии ссылки на скачивание файлов с записями разговоров. Для этого используйте метод $dialer.getCallRecordingPath().
Метод возвращает относительный путь следующего вида:
242829491/2019-06-18/79313365671/16:56:31.55-1PСсылка на полный путь соответствует шаблону:
https://{{host}}/restapi/download/{{projectId}}/recordings/call/{{callRecordingPath}}Здесь:
host— хост, на котором размещен проект, напримерapp.jaicp.com;projectId— целочисленный идентификатор проекта, например242829491;callRecordingPath— значение, которое возвращает$dialer.getCallRecordingPath.