Перейти к основному содержимому

LLM в телефонии

Бета

В телефонном канале вы можете использовать тип ответа llmRequest, чтобы бот в потоковом режиме получал текст от LLM и синтезировал речь.

Бот по предложениям получает текст от LLM и так же по предложениям синтезирует речь. Оба процесса проходят параллельно. Это позволяет уменьшить паузы перед ответом бота по сравнению с тем, когда генерация и синтез проходят последовательно.

Пример последовательной генерации и синтеза без llmRequest
// Бот получает текст от LLM
var llmResponse = $gpt.createChatCompletion([{ "role": "user", "content": $request.query }]);
var response = llmResponse.choices[0].message.content;
// Бот синтезирует речь сразу для всего текста
$reactions.answer(response);

Здесь бот:

  1. Обращается к LLM с помощью $gpt.createChatCompletion. Бот ждет, пока LLM сгенерирует текст полностью.
  2. Отправляет весь текст на синтез речи и ждет, пока речь будет просинтезирована для всего текста.
  3. Воспроизводит текст.

В этом случае между запросом пользователя и ответом бота может возникать длительная пауза в несколько секунд.

llmRequest также позволяет указать фразы, которые бот произнесет, чтобы заполнить паузу в начале генерации текста.

Провайдеры

LLM

Для генерации текстов вы можете использовать:

  • Платформу Caila

    Обращайтесь к моделям сервиса openai-proxy на платформе Caila. LLM доступны только в платном тарифе. Чтобы использовать модели, пополните баланс Caila.

  • Другого провайдера LLM

    Настройте прямое подключение к другому провайдеру. Так вы можете использовать модели, которые недоступны в сервисе openai-proxy.

    Особенности
    • Тип ответа llmRequest поддерживает только LLM, которые совместимы с OpenAI Streaming API. Например, вы можете подключить YandexGPT.

    • Тарификация за запросы к LLM происходит на стороне вашего провайдера.

    • Некоторые провайдеры могу быть недоступны для прямого подключения из РФ.

    Подробнее о настройках смотрите в статье llmRequest.

TTS

Синтез речи работает для любого TTS-провайдера.

Использование llmRequest в сценарии

state: NoMatch
event!: noMatch
script:
$response.replies = $response.replies || [];
$response.replies.push({
type: "llmRequest",
provider: "CAILA_OPEN_AI",
// Модель для генерации текста
model: "gpt-4o",
// Название токена
tokenSecret: "MY_LLM_TOKEN",
// Промт и запрос пользователя
messages: [
{"role": "system", "content": "Отвечай коротко. Максимум несколько предложений."},
{"role": "user","content": $request.query}
]
});

В этом примере llmRequest используется в стейте NoMatch:

  1. Бот отправляет запрос на генерацию текста в сервис openai-proxy на платформе Caila. В поле messages указаны:

    • Промт для LLM, чтобы модель генерировала короткий ответ.
    • Текст запроса пользователя, который хранится в $request.query.
  2. Когда бот получит первое предложение от LLM, он начнет синтезировать речь.

  3. Бот воспроизводит первое предложение пользователю.

  4. Бот продолжает синтезировать и воспроизводить речь по предложениям, пока не получит весь текст от LLM.

предупреждение

После перехода в стейт бот сразу начинает готовить текст и речь для llmRequest. Лимиты LLM и синтеза речи могут быть списаны, даже если пользователь завершил вызов и бот не воспроизвел речь.

Заполнение пауз

В сценарии пауза может возникнуть, пока бот ждет первое предложение текста от LLM. Вы можете заполнить эту паузу двумя способами:

  • Используйте настройку fillersPhraseConfig. Вы можете указать фразу, которую бот произнесет в начале генерации. Это позволит заполнить паузу, если она слишком длинная.

    state: NoMatch
    event!: noMatch
    script:
    $response.replies = $response.replies || [];
    $response.replies.push({
    type: "llmRequest",

    // Бот произнесет фразу, если пауза превысила 2000 мс.
    fillersPhraseConfig: {"fillerPhrase": "Хороший вопрос!", "activationDelayMs": 2000}
    });
  • Укажите другие ответы перед llmRequest. После перехода в стейт бот сразу начинает готовить текст и речь для llmRequest. Бот может выполнять другие реакции перед llmRequest, пока он ждет ответа от LLM.

    state: NoMatch
    event!: noMatch
    # Бот сразу начинает генерировать ответ llmRequest после перехода в стейт
    a: Хороший вопрос!
    a: Дайте подумать
    # Бот уже произнес две фразы. За это время он подготовил часть ответа
    script:
    $response.replies = $response.replies || [];
    $response.replies.push({
    type: "llmRequest",

    });

Перебивание

Пользователь может перебить бота, если бот воспроизводит речь с помощью llmRequest.

В методе $dialer.bargeInResponse укажите режим перебивания forced. Если пользователь перебьет бота, бот прервет речь и не воспроизведет ответ от LLM до конца:

state: NoMatch
event!: noMatch
script:
// Настройки перебивания
$dialer.bargeInResponse({
bargeIn: "forced",
bargeInTrigger: "final",
noInterruptTime: 0
});
// Ответ llmRequest
$response.replies = $response.replies || [];
$response.replies.push({
type: "llmRequest",
provider: "CAILA_OPEN_AI",
model: "gpt-4o",
tokenSecret: "MY_LLM_TOKEN",
messages: [{"role": "user", "content": $request.query}],
});

Перебивание по условию

Вы также можете настроить перебивание по условию. Для этого передайте объект bargeInReply в llmRequest.

В примере ниже:

  1. Бот создает пустой ответ с параметром bargeInIf.
  2. Из этого ответа бот извлекает объект bargeInReply и передает его в llmRequest.
  3. Перебивание срабатывает, если запрос пользователя содержит слово «оператор».

Пример:

state: NoMatch
event!: noMatch
# Создаем пустой ответ с параметром bargeInIf
a: || bargeInIf = "Ответ от LLM"
script:
// Настройки перебивания
$dialer.bargeInResponse({
bargeIn: "forced",
bargeInTrigger: "final",
noInterruptTime: 0
});
// Сохраняем bargeInReply из пустого ответа и удаляем пустой ответ
var bargeInReplyObject = $response.replies.pop().bargeInReply;
// Ответ llmRequest
$response.replies = $response.replies || [];
$response.replies.push({
type: "llmRequest",
provider: "CAILA_OPEN_AI",
model: "gpt-4o",
tokenSecret: "MY_LLM_TOKEN",
messages: [
{"role": "user","content": $request.query}
],
// Передаем объект bargeInReply
bargeInReply: bargeInReplyObject
});

state: BargeInCondition || noContext = true
event!: bargeInIntent
script:
var text = $dialer.getBargeInIntentStatus().text;
// Перебивание срабатывает, если запрос пользователя содержит слово «оператор»
if (text.indexOf("оператор") > -1) {
$dialer.bargeInInterrupt(true);
}
Ограничения

Вызов функций

Вместо генерации текстового ответа LLM может вызвать функцию. В этом случае в сценарий придет событие с названием из параметра eventName. В стейте с этим событием должен быть указан код, который нужно выполнить.

предупреждение
  • Вызов функций поддерживается только для provider: "CUSTOM_LLM".
  • LLM должна поддерживать function calling.
  • Сейчас бот не может завершить звонок с помощью вызова функции. Например, если код функции содержит $dialer.hangUp, то сброс звонка не произойдет.

В примере ниже показан сценарий для бота, который помогает клиентам онлайн-кинотеатра. Для LLM доступны две функции:

  • Если пользователь сообщает о проблеме, то бот с помощью функции reportIssue оставляет сообщение для технической поддержки. В этом сообщении бот указывает только важные детали и самостоятельно устанавливает приоритет.

  • Если пользователь хочет подобрать тариф, то бот уточняет, какое количество устройств пользователь хочет подключить. Далее бот с помощью функции findPlan находит подходящий тариф.

# Описание функций
require: tools.js
# Код для функций
require: functions.js
# Системный промт для LLM
require: prompt.yml
var = prompt

theme: /

state: Start
q!: $regex</start>
a: Здравствуйте!
script: $jsapi.startSession();

# Стейт с llmRequest
state: NoMatch
event!: noMatch
script:
// Системный промт
var systemPrompt = {
role: "system",
// Текст промта из файла prompt.yml
content: prompt.text
};
// История диалога
var history = $jsapi.chatHistoryInLlmFormat();
// История диалога вместе с промтом
var historyWithPrompt = [systemPrompt].concat(history);

// Ответ от LLM
$response.replies = $response.replies || [];
$response.replies.push({
type: "llmRequest",
provider: "CUSTOM_LLM",
model: "example/model-1234",
tokenSecret: "MY_TOKEN",
headers: {"Authorization": "Bearer MY_TOKEN","Content-Type": "application/json"},
url: "https://example.com/api/chat/completions",
// История диалога с промтом
messages: historyWithPrompt,
// Описание функций из файла tools.js
tools: myTools,
// Название события, если LLM вызовет одну из функций
eventName: "toolUsed"
});

# Стейт с событием и кодом функций
state: Tools
event!: toolUsed
script:
// Имя функции, которую вызвала LLM
var toolName = $request.data.eventData.tool_call[0].name;
// Аргументы функции и их значения
var toolArgs = JSON.parse($request.data.eventData.tool_call[0].arguments);

// Если пользователь сообщил о проблеме:
if (toolName === "reportIssue") {
// Функция из файла functions.js
reportIssue(toolArgs.summary, toolArgs.priority);
// Если пользователь хочет подобрать тариф
} else if (toolName === "findPlan") {
// Функция из файла functions.js
findPlan(toolArgs.devices);
}

$reactions.answer("Скажите, чем я еще могу помочь?");

В стейте NoMatch используется типа ответа llmRequest:

  • В historyWithPrompt хранится системный промт и история диалога. Системный промт необходим, чтобы LLM понимала, когда нужно вызвать функцию.
  • В myTools указаны описания функций, которые может вызвать LLM.
  • В параметре eventName указано название события, которое будет приходить в сценарий, если LLM вызвала одну из функций.

Стейт Tools обрабатывает событие toolUsed:

  • В стейте указаны условия по имени функции.

  • Если была вызвана reportIssue, то бот выполнит одноименную функцию из functions.js — запишет в аналитику комментарий о проблеме.

    подсказка

    Если LLM вызывает функцию вместо генерации ответа, то в аналитике будет отображается фраза бота в формате: CollectedToolCalls(eventName=toolUsed, toolCalls=[ToolCall(index=0, id=abcde-12345, type=function, name=findPlan, arguments={"devices": 5})]).

  • Если была вызвана findPlan, то бот выполнит одноименную функцию из functions.js — сообщит название тарифа пользователю.