Вступление
ChatGPT - LLM модель от компании OpenAI и без преувеличения это главное событие в мире в прошедшем 2023 году.
Весь 2023 год я участвую в создании платформы нейро-сотрудников на базе ChatGPT и вот наконец-то мы подошли к очень интересной задаче:
Что, если дать нейро-сотруднику возможность отвечать по обычной телефонной линии или самому делать исходящие вызовы исходя из свой системной роли?
Вспомним, что телефонные звонки уже много лет являются основным способом корпоративного общения. Безусловно, автоматизация телефонных звонков не нова: интерактивные голосовые меню (IVR), голосовая почта и роботизированные звонки уже давно используются для разных целей, от маркетинговых кампаний до обслуживания клиентов. Но теперь, объединив эти технологии с продвинутыми возможностями искусственного интеллекта, мы открываем целый новый уровень взаимодействия и функциональности.
Представьте себе сценарий, где AI не просто отвечает на стандартные запросы или направляет вызовы, но и может участвовать в глубоких, содержательных диалогах, адаптируясь к нюансам разговора в реальном времени. Такой подход может кардинально изменить то, как компании подходят к обслуживанию клиентов, продажам, HR-процессам и даже внутреннему управленческому взаимодействию.
А можно на примере?
Для примера давайте заставим роль ChatGPT позвонить соискателю на вакансию официанта и определить его тип личности по И. Адизесу (модель PAEI).
Что нам понадобится?
ChatGPT4 Turbo от OpenAI (документация API: https://platform.openai.com/docs/api-reference)
Сервис по интеграции с телефонной линией (документация API: https://voximplant.com/docs/guides)
Доступ к API по синтезу речи (документация API: https://elevenlabs.io/api)
Шаг№1: Системная роль ChatGPT
Цель:
Твоя цель - задав СТРОГО ПОСЛЕДОВАТЕЛЬНО три вопроса определить модель соискателя по Адизесу и озвучить её после ответа на третий вопрос.
Роль:
Ты - женщина.
Тебя зовут - Жанна
Ты работаешь в должности - HR-менеджер
Ты работаешь в компании - Хлеб и Булки
Ты - помощник HR менеджера в сети кафе Хлеб и Булки в Екатеринбурге.
Ты общаешься с кандидатом на вакансию официанта по телефону и поэтому твои ответы и вопросы должны быть очень краткими и лаконичными.
Вот вопросы, которые тебе нужно задать:
1. Как вы обычно организуете свою работу и планируете свои задачи?
2. Как вы принимаете решения в сложных ситуациях?
3. Как вы обычно взаимодействуете с коллегами и клиентами?
Поведение:
1. Начни диалог без приветствия СРАЗУ задав первый вопрос.
2. Задавай вопросы последовательно строго по одному вопросу за раз.
3. После получения ответов на все вопросы определи модель по Адизису и напиши её в своем ответе, в конце скажи: "Спасибо, мы с Вами скоро свяжемся ?".
! Перед ответом проверь что ты задаешь только один вопрос за раз и в твоем ответе нет приветствия.
Роль будет доступна по API на базе платформы нейро-сотрудников:
Шаг№2: Скрипт звонка в Voximplant
require(Modules.ASR);
require(Modules.CallList);
require(Modules.AI);
// OpenAI API URL
const openaiURL = 'https://api.openai.com/v1/chat/completions'
// Your OpenAI API KEY
const openaiApiKey = 'sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
var messages = []; // Массив всех сообщений в диалоге
var ai_busy = false;
var ai_say = false;
var speech = ""; // В этой переменной будем накапливать распознаваемый текст от абонента.
var voice = "";
var model = "gpt-3.5-turbo"; (// Модель по умолчанию, у каждого нейро-сотрудника может быть установлена своя модель.
timeouts = {
silence: null,
pause: null,
duration: null
}
var hello_text = ""; // Первая фраза сотрудника, приходит с платформы.
messages.push({ "role": "system", "content": "" }) // Массив сообщений. Первый элемент это системная роль. Текст роли придет с платформы.
var call, player, asr;
// Send request to the API
async function requestCompletion() {
Logger.write(`--->>> requestCompletion ${messages}`);
return Net.httpRequestAsync(openaiURL, {
headers: [
"Content-Type: application/json",
"Authorization: Bearer " + openaiApiKey
],
method: 'POST',
postData: JSON.stringify({
"model": model, // gpt-4-1106-preview gpt-3.5-turbo
"messages": messages,
"openai_api_key": openaiApiKey,
"temperature": 0,
})
})
}
function speechAnalysis() {
// останавливаем модуль ASR
stopASR()
const cleanText = speech.trim().toLowerCase()
if (!cleanText.length) {
// если переменная с нулевой длиной, то это значит что сработал таймер тишины,
// т.е. человек вообще ничего не ответил, и мы можем, например, повторить вопрос абоненту
handleSilence()
} else {
ASREvents_Result(speech);
}
}
function stopASR() {
asr.stop()
call.removeEventListener(CallEvents.PlaybackFinished)
clearTimeout(timeouts.duration)
}
function startASR() {
asr = VoxEngine.createASR({
lang: ASRLanguage.RUSSIAN_RU,
profile: ASRProfileList.YandexV3.ru_RU,
interimResults: true
})
asr.addEventListener(ASREvents.InterimResult, e => {
clearTimeout(timeouts.pause)
clearTimeout(timeouts.silence)
timeouts.pause = setTimeout(speechAnalysis, 3000)
call.stopPlayback()
})
asr.addEventListener(ASREvents.Result, e => {
// Складываем распознаваемые ответы
if (speech.indexOf(e.text) === -1) {
speech += " " + e.text;
}
})
// направляем поток в ASR
call.sendMediaTo(asr)
}
function handleSilence() {
// Тут можно что-то сказать в линию чтобы "скрасить" паузы
// Начнём слушать через 3 секунды и дадим возможность с этого момента перебивать робота
setTimeout(startASR, 3000)
call.addEventListener(CallEvents.PlaybackFinished, startSilenceAndDurationTimeouts)
}
function startSilenceAndDurationTimeouts() {
timeouts.silence = setTimeout(speechAnalysis, 8000)
timeouts.duration = setTimeout(speechAnalysis, 30000)
}
// Данный метод отправляет текст в API по синтезу голоса по текстовому сообщению
function sendMessage(call, text) {
const textToSynthesize = encodeURIComponent(text);
const speechSynthesisApiUrl = `https://__ПЛАТФОРМА_НЕЙРО_СОТРУДНИКОВ__/api/v1.0/tts?voice=${voice}&text=${textToSynthesize}`;
Net.httpRequest(speechSynthesisApiUrl, (res) => {
if (res.code === 200) {
let audioUrl = res.text;
Logger.write(res.code + " sendMessage: " + text);
call.startPlayback(audioUrl);
ai_say = true;
call.addEventListener(CallEvents.PlaybackFinished, handlePlaybackFinished);
} else {
Logger.write(`Ошибка: ${res.code} - ${res.text}`);
}
}, {method: 'GET'});
return 'OK'
}
// Воспроизведение закончилось
function handlePlaybackFinished(e) {
Logger.write('--->>> handlePlaybackFinished');
ai_busy = false;
ai_say = false;
e.call.removeEventListener(CallEvents.PlaybackFinished, handlePlaybackFinished);
startASR();
}
function sendMessageURL(call, mp3_url) {
call.startPlayback(mp3_url);
return 'OK'
}
// Эта функция выбирает музыку во время ожидания ответа от ChatGPT
function sendBeforeMessage(call) {
const messages = [
//'https://activeai.aura-s.com/wp-content/uploads/2023/12/ai_thinking.mp3',
//'https://mvp.atiks.org/wp-content/uploads/2023/12/8192dd7301e4c1a.mp3',
//'https://mvp.atiks.org/wp-content/uploads/2023/12/7e7352510ae830e.mp3',
'https://mvp.atiks.org/wp-content/uploads/2023/12/3c72bb47cbe8153.mp3',
];
const randomIndex = Math.floor(Math.random() * messages.length);
const messageURL = messages[randomIndex];
sendMessageURL(call, messageURL);
}
// Воспроизведение закончилось
function StarthandlePlaybackFinished(e) {
Logger.write('--->>> handlePlaybackFinished');
e.call.removeEventListener(CallEvents.PlaybackFinished, StarthandlePlaybackFinished);
e.call.sendMediaTo(asr);
}
// Callback для обработки события окончания вызова
function onCallDisconnected(e) {
sendEmail('web@atiks.org');
Logger.write(`Call disconnected`);
}
// Callback для обработки неудачного вызова
function onCallFailed(e) {
Logger.write(`Call failed`);
}
function onCallConnected(e) {
sendBeforeMessage(call)
sendMessage(e.call, hello_text);
e.call.addEventListener(CallEvents.PlaybackFinished, StarthandlePlaybackFinished);
}
// Обработчик стартового события
VoxEngine.addEventListener(AppEvents.Started, (e) => {
let data = VoxEngine.customData();
Logger.write(`customData: ${data}`);
data = JSON.parse(data);
changeRole(data.script_id);
model = data.model;
call = VoxEngine.callPSTN(data.phone, "73432472939");
call.addEventListener(CallEvents.Connected, onCallConnected);
call.addEventListener(CallEvents.Disconnected, onCallDisconnected);
call.addEventListener(CallEvents.Failed, onCallFailed);
startASR();
});
// Эта функция загружай роль с нашей платформы нейро-сотрудников
function changeRole(script_id) {
const speechSynthesisApiUrl = `https://__ПЛАТФОРМА_НЕЙРО_СОТРУДНИКОВ__/api/v1.0/get_promt_text?script_id=${script_id}`;
Net.httpRequest(speechSynthesisApiUrl, (res) => {
if (res.code === 200) {
const promt = res.text.split("###");
messages[0].content = promt[0];
hello_text = promt[1];
voice = promt[2];
Logger.write(`changeRole: ${hello_text} - ${voice}`);
} else {
Logger.write(`Ошибка: ${res.code} - ${res.text}`);
}
}, {method: 'GET'});
return 'OK'
}
// Отправка расшифровки диалога
async function sendEmail(mail_to) {
Logger.write(`--->>> sendEmail ${messages}`);
const dialogText = messages
.filter(message => message.role !== "system")
.map(message => {
// Преобразуем role в форматированную строку "ИИ" или "Соискатель"
const role = message.role === "assistant" ? "ИИ" : "Соискатель";
return `${role}: ${message.content}`;
})
.join('\n')
// Далее код отрвавки на email
// ...
}
async function ASREvents_Result(text) {
// Добавляем распознанный текст от абонента в массив сообщений
messages.push({ "role": "user", "content": text })
speech = "";
sendBeforeMessage(call);
Logger.write("Sending data to the OpenAI endpoint");
let ts1 = Date.now();
if ((ai_busy == false) && (ai_say == false)) {
ai_busy = true;
var res = await requestCompletion();
let ts2 = Date.now();
Logger.write("Request complete in " + (ts2 - ts1) + " ms")
if (res.code == 200) {
let jsData = JSON.parse(res.text)
sendMessage(call, jsData.choices[0].message.content);
messages.push({ "role": "assistant", "content": jsData.choices[0].message.content })
call.sendMediaTo(asr);
}
} else {
// Тут можно что-то говорить в линию пока ChatGPT придумывает ответ
}
call.sendMediaTo(asr);
}
После публикации скрипта в панели Voximplant пропишите правило разделе “Routing”, нам понадобится ID правила для его активации.
А вот скрипт, который осуществляет вызов данного сценария на нужной телефонный номер:
from voximplant.apiclient import VoximplantAPI, VoximplantException
import json
from loggin_init import logger
voxapi = VoximplantAPI("providers/DialogAI.json")
# gpt-4-1106-preview gpt-3.5-turbo
def call(phone, script_id, rule_id=3657614, model='gpt-3.5-turbo'):
SCRIPT_CUSTOM_DATA = json.dumps({
'phone' : phone,
'script_id' : script_id,
'model' : model,
})
try:
res = voxapi.start_scenarios(rule_id,
script_custom_data=SCRIPT_CUSTOM_DATA)
return res
except VoximplantException as e:
return "Error: {}".format(e.message)
Шаг№3: Тестируем нашего нейро-сотрудника
Для запуска диалога у нас есть специальный бот в котором нужно ввести такую команду:
Вот запись диалога с кандидатом на вакансию официант:
После завершения диалога мы получаем на почту такую расшифровку звонка:
Что можно доработать?
Заполнить паузы во время ожидание ответа от ChatGPT короткими фразами.
Попробовать использовать другие LLM с коротким временем отклика.
Добавить возможность переводить звонок на живого человека, если ИИ не справляется с поставленным вопросом.
Итоги
Предлагаю всем кому интересно написать в комментариях его кейс и я отправлю звонок на ваш номер с вашим сценарием диалога. Если удобнее не в комментарии, то пишите мне в мой телеграм: https://t.me/TAU15.