This site is no longer updated.Go to new Conversational Cloud docs

Сценарий бота для голосовой рассылки


В голосовых рассылках используются синтез и распознавание речи.

Синтез речи — процесс генерирования речи по печатному тексту.

Распознавание речи — процесс преобразования речи в текст.

В связи с этим написание сценария бота имеет свои особенности. В этой статье рассмотрим:


Адаптация сценария для телефонного канала


Распознавание

При адаптации сценария бота для телефонного канала следует отлавливать и обрабатывать событие 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.

Разработчик сценария может добавить колонки в отчет, которые требуются для определенной рассылки, например:

  1. Для получения данных в ходе звонка:
    • $dialer.reportData("ФИО", "Иванов Иван") — в отчете по результатам обзвона будет добавлена колонка ФИО со значением Иванов Иван.
  2. Для фиксирования фразы и факта прохождения по этапу:
    • $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.