Pull to refresh

Может ли нейро-сотрудник на базе ChatGPT звонить по обычной телефонной линии?

Level of difficultyMedium
Reading time8 min
Views7.7K
Фото получено через DALL·E 3 по запросу “сгенери мне фото для статьи в которой роль ChatGPT звонит по телефону потенциальному соискателю на вакансию и определяет модель личности по Адизесу”
Фото получено через DALL·E 3 по запросу “сгенери мне фото для статьи в которой роль ChatGPT звонит по телефону потенциальному соискателю на вакансию и определяет модель личности по Адизесу”

Вступление

ChatGPT - LLM модель от компании OpenAI и без преувеличения это главное событие в мире в прошедшем 2023 году.

Весь 2023 год я участвую в создании платформы нейро-сотрудников на базе ChatGPT и вот наконец-то мы подошли к очень интересной задаче:

Что, если дать нейро-сотруднику возможность отвечать по обычной телефонной линии или самому делать исходящие вызовы исходя из свой системной роли?

Вспомним, что телефонные звонки уже много лет являются основным способом корпоративного общения. Безусловно, автоматизация телефонных звонков не нова: интерактивные голосовые меню (IVR), голосовая почта и роботизированные звонки уже давно используются для разных целей, от маркетинговых кампаний до обслуживания клиентов. Но теперь, объединив эти технологии с продвинутыми возможностями искусственного интеллекта, мы открываем целый новый уровень взаимодействия и функциональности.

Представьте себе сценарий, где AI не просто отвечает на стандартные запросы или направляет вызовы, но и может участвовать в глубоких, содержательных диалогах, адаптируясь к нюансам разговора в реальном времени. Такой подход может кардинально изменить то, как компании подходят к обслуживанию клиентов, продажам, HR-процессам и даже внутреннему управленческому взаимодействию.

А можно на примере?

Фото получено через DALL·E 3 по запросу “сгерени фото соискателя на вакансию официанта который проходит телефонное интервью”
Фото получено через DALL·E 3 по запросу “сгерени фото соискателя на вакансию официанта который проходит телефонное интервью”

Для примера давайте заставим роль ChatGPT позвонить соискателю на вакансию официанта и определить его тип личности по И. Адизесу (модель PAEI).

Что нам понадобится?

  1. ChatGPT4 Turbo от OpenAI (документация API: https://platform.openai.com/docs/api-reference)

  2. Сервис по интеграции с телефонной линией (документация API: https://voximplant.com/docs/guides)

  3. Доступ к 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 правила для его активации.

Создайте маршрут для нашего сценария в разделе "Routing"
Создайте маршрут для нашего сценария в разделе "Routing"

А вот скрипт, который осуществляет вызов данного сценария на нужной телефонный номер:

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: Тестируем нашего нейро-сотрудника

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

Вызываем исходящий диалог, для этого указываем номер нашего нейро-сотрудника на платформе и номер, куда он должен позвонить
Вызываем исходящий диалог, для этого указываем номер нашего нейро-сотрудника на платформе и номер, куда он должен позвонить

Вот запись диалога с кандидатом на вакансию официант:

После завершения диалога мы получаем на почту такую расшифровку звонка:

Такое письмо приходит на почту при завершении звонка
Такое письмо приходит на почту при завершении звонка

Что можно доработать?

  1. Заполнить паузы во время ожидание ответа от ChatGPT короткими фразами.

  2. Попробовать использовать другие LLM с коротким временем отклика.

  3. Добавить возможность переводить звонок на живого человека, если ИИ не справляется с поставленным вопросом.

Итоги

Предлагаю всем кому интересно написать в комментариях его кейс и я отправлю звонок на ваш номер с вашим сценарием диалога. Если удобнее не в комментарии, то пишите мне в мой телеграм: https://t.me/TAU15.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 11: ↑7 and ↓4+6
Comments19

Articles