О ChatGPT сейчас не говорит только ленивый. Но ему чего-то не хватает, например голоса. Давайте попробуем соединить голосовой помощник Алиса и ChatGPT. Таким образом мы сможем взаимодействовать с ChatGPT с помощью голоса. А он с помощью голоса может нам отвечать. Конечно тут будут ограничения. Я подробно опишу их дальше. Данная статья не столько о ChatGPT, сколько о том, как писать навыки для Алисы. Было интересно разобраться и написать такой навык за вечер.

ChatGPT API

Официальное API ChatGPT открыто и для его использования нужен только API_KEY c сайта OpenAI https://platform.openai.com/account/api-keys. Апи платное, есть триал и лимит бесплатного использования на первые три месяца. Сейчас,похоже, чтобы зайти туда нужен VPN.

Для взаимодействия будем пользоваться официальной питоновской библиотекой openai.

Код взаимодействия с chatGPT моделью:

import os

import openai

OPENAI_API_KEY = os.environ['OPENAI_API_KEY']
async def aquery(message, prev_messages=None):
    messages = []
    if not prev_messages:
        all_messages = []
    else:
        all_messages = prev_messages.copy()
    all_messages.append(message)
    for m in all_messages:
        messages.append({"role": "user", "content": m})

    chat = await openai.ChatCompletion.acreate(model="gpt-3.5-turbo", messages = messages)
    reply = chat.choices[0].message.content
    reply = reply.strip()
    return reply

aquery принимает текст запроса к chatGPT, а также опционально список предыдущих запросов prev_messages. Так мы сможем держать chatGPT в контексте наших предыдущих сообщений и можем поддерживать диалог.
openai.ChatCompletion.acreate функция запроса к chatGPT.
model это модель которую надо использовать.
messages список объектов запросов.

Вот по сути и весь код взаимодействия с ChatGPT API.

Навыки для Алисы

Что же такое навык для Алисы? Навык можно рассматривать как какую-то подпрограмму которая Алиса запускает, когда произносишь специальную фразу активации. Далее общение происходит напрямую с навыком, минуя Алису. Единственная фраза, которую она обрабатывает в этом режиме "Алиса, хватит".

Технически навык это выделенный post https endpoint. Алиса посылает POST запрос с текстом озвученным пользователем и ожидает ответ с тем тексто, который она должна озвучить. Навыки можно создавать и на платформе Yandex Cloud Functions. Для этого надо зарегистрироваться на Yandex Cloud и создать платежный аккаунт. Но Cloud Functions для навыков Алисы не тарифицируется. К сожалению создание платежного аккаунта доступно только для пользователей из России и Казахстана. Поэтому мы будем делать наш собственный сервер. Для этого надо иметь сервер с IP доступным извне, доменом и включенным https. Будем считать что все это у нас уже есть, настроено и работает.

Разработка сервера

Первая проблема вытекает именно из того что Алиса посылает запрос на наш сервер и ждет. Алиса ждет ровно 3 секунды. Если ответ не приходит за это время, Алиса скажет "Навык не отвечает" и просто выкинет вас из навыка. В это время входит и время отправки запроса и получения ответа, так что на обработку запроса остается совсем мало времени.
Понятно что генерация ответа у GPT займет больше. На этот случай после запроса пользователя мы просто просим подождать и позвать нас позже. Чтобы пользователю было не скучно можно, например, проиграть музыку, которую мы загрузили в навык. Однако навык не может инициализировать разговор, потому пользователь должен сказать что-то, чтобы активировать навык позже. Да, это достаточно неудобно и приходится каждый раз спрашивать навык о том готов ли ответ.

Начнем с запуска сервера. Будем использовать FastAPI.

main.py

from fastapi import FastAPI, Request

from dotenv import load_dotenv
load_dotenv()

app = FastAPI()

@app.post("/post")
async def post(request: Request):
    request = await request.json()
    response = {
        'session': request['session'],
        'version': request['version'],
        'session_state': request.get('state', {}).get('session', {}),
        'response': {
            'end_session': False
        }
    }
    ## Заполняем необходимую информацию
    await handle_dialog(response, request)
    print(response)
    return response

Запускаем сервер вот так

uvicorn main:app --host 0.0.0.0 --port 5000 

Тут мы просто написали обработчик POST json запросов. Получаем запрос
(https://yandex.ru/dev/dialogs/alice/doc/request.html). Подготавливаем словарь, который будем возвращать с нашим ответом (https://yandex.ru/dev/dialogs/alice/doc/response.html).
Подробнее о запросах и ответах можно прочитать по данным ссылкам. Скажу только что в большинстве случаев нам будет нужен только текст запроса пользователя. request['request']['original_utterance']. А ответ мы вернем в response['response']['text']. Конечно в запросе есть и другие поля. Яндекс проводит обработку текста запроса и возвращает это нам, например можно получить именованные сущности которые назвал пользователь в своем запросе, например имена или адреса, или интенты.
session_state позволяет хранить данным между запросами к навыку, на первое время этого будет достаточно и так мы сможем сохранять контекст и поддерживать беседу с пользователем.

Функция handle_dialog будет отвечать за обработку запроса и отправку ответа от chatGPT.

CUT_WORD = ['Алиса', 'алиса']
answers = dict()


async def handle_dialog(res,req):
    if req['request']['original_utterance']:
        # подтягиваем предыдущие сообщения от пользователя, которых мы сохранили в навыке        
        session_state = res.get('session_state', {})
        messages = session_state.get('messages', [])
        
        # получаем текст запроса от пользователя     
        request = req['request']['original_utterance']
        
        # Если Алиса была активирована то мы случайно может отправить Алиса первым словом в запросе
        for word in CUT_WORD:
            request = request.lstrip(word)

        request = request.strip()

        # Если мы уже ответили на все вопросы то слушаем текущий вопрос
        if 'message' not in session_state:
            # асинхронно обращаемся к chatGPT 
            task = asyncio.create_task(ask(request, messages))
            
            # Ждем в призрачной надежде что апи успеет дать ответ за 1 секунду
            await asyncio.sleep(1)
            
            # сохраняем контекст предыдыущих запросов в навыке
            messages.append(request)
            session_state['messages'] = messages

            if task.done():
                # Если мы успели получить ответ просто отвечаем пользователю
                reply = task.result()
                del answers[request]
            else:
                reply = 'Не успел получить ответ. Спросите позже'
                session_state['message'] = request
        else:
            # Если мы не успели ответить на предыдущий вопрос то игнорируем ввод пользователя 
            # пока не ответим на предыдущий вопрос old_request
            old_request = session_state['message']
            
            # ответа все еще нет :(
            if old_request not in answers:
                reply = 'Ответ пока не готов, спросите позже'
            else:
                # Ответ на предыдущий вопрос готов.
                # возвращаем его пользователю
                answer = answers[old_request]
                del answers[old_request]
                del session_state['message']
                reply = f'Отвечаю на предыдущий вопрос "{old_request}"\n {answer}'
    else:
        ## Если это первое сообщение — представляемся
        reply = 'Я умный chat бот. Спроси что-нибудь'
    res['response']['text'] = reply

req['request']['original_utterance'] может быть пустым. Это значит что пользователь только активировал навык. Приветствуем его.

Мы завели словарь answer. В этом словаре ключ - вопрос пользователя, а значение ответ нейросети. Конечно, лучше хранить сессии пользователя вместо этого и не памяти а, например, на Redis.

В коде мы запрашиваем ответ у chatGPT если он не уложился в 1 секунду, то отвечаем пользователю, что пока не готовы и предлагаем спросить нас позже.

Все следующие запросы пользователя мы игнорируем. Они нужны нам только для того что активировать запрос навыка, так как навык не может инициировать разговор.

Если при повторном запросе ответ все еще не готов мы снова просим повторить запрос позже.

Также мы храним историю предыдущих запросов.

Если с момента предыдущего запроса к навыку прошло достаточно времени лампочка на Алисе тухнет и чтобы задать следующий вопрос мы должны ее разбудить. Очевидно единственный способ это позвать колонку и после этого задать вопрос. Однако, если мы не смотрим на колонку, мы не узнаем активирована она еще или нет. Именно поэтому мы используем CUT_WORD. Мы просто удаляем Имя колонки из начала запроса. ChatGPT это видеть незачем.

Ну и последняя функция.

async def ask(request, messages):
    try:
        reply = await gpt.aquery(request, messages)
    except Exception as e:
        traceback.print_exc()
        reply = 'Не удалось получить ответ'
    answers[request] = reply
    return reply

Это обертка над нашей функцией обращения к GPT-3 модели. Обрабатывает ошибки и обновляет наш словарь answers.

В принципе это весь код, его можно найти на Github.

Публикация навыка

Дальше нам надо опубликовать навык. Наш обработчик должен быть доступен с серверов яндекса по https. Кроме того сервер должен иметь публичный IP.

Подробнее о том как подключить новый навык на платформе диалогов Яндекса хорошо написано в инструкции

В качестве Webhook URL используем URL нашего обработчика

Адрес обработчика
Адрес обработчика


Если мы не хотим публиковать навык мы можем сделать его приватным. В этом случае даже не надо отправлять его на модерацию и навыком сможете воспользоваться только вы или те у кого есть специальное приглашение.

Создание приватного навыка
Создание приватного навыка

Особое внимание следует уделить Активационные имена и Примеры запросов. Это список фраз которые мы можем использовать, чтобы запустить навык. Имена должны быть достаточно уникальными с одной стороны чтобы не совпадать с фразами других возможных навыков. С другой стороны Алиса должна различать эти фразы и правильно их воспринимать не путая с другими словами. Если не соблюдать это правило вы потратите много времени доказывая Алисе, что вы хотите запустить.

Вот и все,наш первый навык готов.

В конце прикреплю пару примеров работы навыка.

Скриншоты из мобильного приложения

Заключение

Вот несколько выводов которые хотелось бы указать в заключении.

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

  • Алиса имеет ограничение на время запроса в три секунды. Если не укладываемся на UX взаимодействия сильно падает и мы должны просить пользователя активировать нас позже.

  • Имеется ограничение на длину ответа в 1024 символов. Мы должны сделать пагинацию для длинных ответов.

  • Алиса не сможет прочитать нам код, потому запросы на генерацию кода лучше делать на сайте.

  • Навык должен отвечать быстро и кратко, голосовое взаимодействие занимает много времени и не надо отнимать его у пользователя еще больше

Комментарии (16)


  1. achekalin
    00.00.0000 00:00
    +11

    Почему-то подумалось: пусть ChatGPT довольно легко поймать на том, что он отвечает не по делу, а чисто "на тему вопроса" (собственно, как говорится, обратного никто и не обещал), но вот от Алисы (ну и, будем честными, от Алисы, насмотревшись на работу Siri на исходной презентации) лично я ждал, что это будет не тупо "Алиса, реши мою проблему -- Я умею только делать то, что вы точно скажете, а не проблемы решать", а именно собеседник, который будет следить за контекстом и додумывать подробности.

    И вот сейчас к просто говорящей колонке, умеющей отправлять запрос в поисковик (в т.ч. и тематический, типа кинопоиска - но не умеющей среди ответов выбирать подходящий именно спрашивающему) так и не ушедшей сильно дальше описанного за годы, прикручивают именно навык быть собеседником - почему-то это сделал не Яндекс в базовую логику Алисы, а именно сторонний автор и в виде "навыка".

    P.S. Иногда кажется, что компании на букву Я стало (финансово, в первую очередь - и сейчас эта составляющая окончательно стала первичной) интереснее быть посредником в вызове такси и в доставке еды, нежели оставаться лидером в поиске и делании жизнь людей лучше в широком смысле.


    1. Krey
      00.00.0000 00:00
      +6

      Так яндекс делает для Алисы свою БЯМ. Скоро будет.


      1. root4joy
        00.00.0000 00:00
        +1

        новое стоп-слово


    1. white-wild
      00.00.0000 00:00

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


  1. Chelidonium
    00.00.0000 00:00

    кодинг это отдельная песня, но
    диалог и общение подразумевает самосознание и разум, а этого нет у ChatGPT,
    это всё 'беседы' с неразумным бессознательным, имитация суррогата общения,
    и вероятно вот уже и очевидное начало побочных эффектов :


    1. ssj100
      00.00.0000 00:00
      +1

      Разве это побочный эффект? 2 месяца мог нормально общаться. может это 2 самых лучших месяца в его жизни.


  1. janvarev
    00.00.0000 00:00
    +6

    М-да. Почитал про Алису, и прям начинаю быть доволен, что в моей опенсорсной Ирине этот навык прикрутить проще (недавняя Хабр-статья). Не надо укладываться в 3 секунды; не надо ограничивать себя в символах; не надо заморачиваться с онлайн-вебхуком на стороннем сервере. Собственно, код всего плагина - 75 строк.


    1. ArtyomPozharov
      00.00.0000 00:00

      Не работал с Ириной. Но концепция уже лучше. Кто будет пользоваться Алисой в какой-нибудь Украине, где Яндекс заблокирован. Да и платить деньги этой прогнившей мутной галере - не комильфо. Лучше занести деньги разработчику на развитие свободного проекта. При этом размещать Ирину можно и на своëм сервере. Кода меньше, ограничений нет. Класс.


      1. root4joy
        00.00.0000 00:00

        в дусю проще добавить


  1. nervix
    00.00.0000 00:00
    +2

    Амазоновская алекса ждет 8 секунд, что немного удобней, но openai иногда и с этим не справляется


  1. AnatolyBelov
    00.00.0000 00:00

    Добрый день )
    Реально работающий навык с ChatGPT по данной статье есть в Алисе?
    Как вызвать ?


    1. Berserkr
      00.00.0000 00:00
      +1

      А кто платить будет? Запросы по апи платные. Для себя если сделать то хатит стартового баланса который всем раздавали, но для пабличка этого и на час не хватит.


      1. riky
        00.00.0000 00:00

        можно дать пользователям вводить свои токены. хотя бы через веб интерфейс.


    1. MrFrederic
      00.00.0000 00:00

      Есть, но Яндекс не даёт и не даст его опубликовать потому что это бы нарушило политику использования OpenAI


  1. freylis
    00.00.0000 00:00
    +4

    Благодаря вашей статье мне пришла мысль подружить мою Алису с авиасимулятором, научив Алису выполнять команды, отвлекающие непосредственно от пилотирования. Спасибо)


  1. Piramind
    00.00.0000 00:00

    Хорошая статья) Тоже появилась идея с навыком по заметкам взамен существующей функции.