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);
Здесь бот:
- Обращается к LLM с помощью
$gpt.createChatCompletion
. Бот ждет, пока LLM сгенерирует текст полностью. - Отправляет весь текст на синтез речи и ждет, пока речь будет просинтезирована для всего текста.
- Воспроизводит текст.
В этом случае между запросом пользователя и ответом бота может возникать длительная пауза в несколько секунд.
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
:
-
Бот отправляет запрос на генерацию текста в сервис
openai-proxy
на платформе Caila. В полеmessages
указаны:- Промт для LLM, чтобы модель генерировала короткий ответ.
- Текст запроса пользователя, который хранится в
$request.query
.
-
Когда бот получит первое предложение от LLM, он начнет синтезировать речь.
-
Бот воспроизводит первое предложение пользователю.
-
Бот продолжает синтезировать и воспроизводить речь по предложениям, пока не получит весь текст от 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
.
В примере ниже:
- Бот создает пустой ответ с параметром
bargeInIf
. - Из этого ответа бот извлекает объект
bargeInReply
и передает его вllmRequest
. - Перебивание срабатывает, если запрос пользователя содержит слово «оператор».
Пример:
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);
}
- Контекстное перебивание не поддерживается для
llmRequest
. - Если пользователь перебил бота, то текст
llmRequest
не будет доступен в истории диалога, например в методе$jsapi.chatHistoryInLlmFormat
. Вы можете посмотреть текст ответа в аналитике.
Вызов функций
Вместо генерации текстового ответа LLM может вызвать функцию.
В этом случае в сценарий придет событие с названием из параметра eventName
.
В стейте с этим событием должен быть указан код, который нужно выполнить.
- Вызов функций поддерживается только для
provider: "CUSTOM_LLM"
. - LLM должна поддерживать function calling.
- Сейчас бот не может завершить звонок с помощью вызова функции.
Например, если код функции содержит
$dialer.hangUp
, то сброс звонка не произойдет.
В примере ниже показан сценарий для бота, который помогает клиентам онлайн-кинотеатра. Для LLM доступны две функции:
-
Если пользователь сообщает о проблеме, то бот с помощью функции
reportIssue
оставляет сообщение для технической поддержки. В этом сообщении бот указывает только важные детали и самостоятельно устанавливает приоритет. -
Если пользователь хочет подобрать тариф, то бот уточняет, какое количество устройств пользователь хочет подключить. Далее бот с помощью функции
findPlan
находит подходящий тариф.
- main.sc
- prompt.yml
- tools.js
- functions.js
# Описание функций
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("Скажите, чем я еще могу помочь?");
text: |
Ты голосовой помощник онлайн-кинотеатра.
Пользователь может обратиться с двумя типами запросов.
1. Если он сообщает о проблеме (например, не работает фильм, ошибка при оплате и т.д.),
вызови функцию reportIssue.
2. Если пользователь хочет подобрать тариф, узнай, сколько устройств он хочет подключить.
Затем вызови функцию findPlan, передав это число как параметр.
Отвечай кратко, понятно и дружелюбно. Не придумывай ничего от себя.
var myTools = [
{
"type": "function",
"function": {
"name": "reportIssue",
"description": "Сообщить поддержке о проблеме пользователя",
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "Только важные детали проблемы"
},
"priority": {
"type": "string",
"description": "Приоритет. Определи его сам",
"enum": ["low", "medium", "high"]
}
},
"required": [
"summary",
"priority"
]
}
}
},
{
"type": "function",
"function": {
"name": "findPlan",
"description": "Подобрать тарифы по количеству устройств",
"parameters": {
"type": "object",
"properties": {
"devices": {
"type": "integer",
"description": "Сколько устройств пользователь хочет подключить"
}
},
"required": ["devices"]
}
}
}
];
function reportIssue(summary, priority) {
// Бот записывает комментарий в аналитику
$analytics.setComment(summary + " | Приоритет: " + priority);
// Бот отвечает пользователю
$reactions.answer("Спасибо! Мы передали информацию в поддержку.");
};
function findPlan(devices) {
// Список тарифов
var plans = [
{ name: "Базовый", max: 1 },
{ name: "Стандарт", max: 3 },
{ name: "Премиум", max: 5 }
];
// Находим первый подходящий тариф
var plan = null;
for (var i = 0; i < plans.length; i++) {
if (plans[i].max >= devices) {
plan = plans[i];
break;
}
}
// Ответ пользователю
if (plan) {
$reactions.answer("Вам подойдёт тариф: " + plan.name);
} else {
$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
— сообщит название тарифа пользователю.