
Вступление
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.