О 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)
Chelidonium
00.00.0000 00:00кодинг это отдельная песня, но
диалог и общение подразумевает самосознание и разум, а этого нет у ChatGPT,
это всё 'беседы' с неразумным бессознательным, имитация суррогата общения,
и вероятно вот уже и очевидное начало побочных эффектов :ssj100
00.00.0000 00:00+1Разве это побочный эффект? 2 месяца мог нормально общаться. может это 2 самых лучших месяца в его жизни.
janvarev
00.00.0000 00:00+6М-да. Почитал про Алису, и прям начинаю быть доволен, что в моей опенсорсной Ирине этот навык прикрутить проще (недавняя Хабр-статья). Не надо укладываться в 3 секунды; не надо ограничивать себя в символах; не надо заморачиваться с онлайн-вебхуком на стороннем сервере. Собственно, код всего плагина - 75 строк.
ArtyomPozharov
00.00.0000 00:00Не работал с Ириной. Но концепция уже лучше. Кто будет пользоваться Алисой в какой-нибудь Украине, где Яндекс заблокирован. Да и платить деньги этой прогнившей мутной галере - не комильфо. Лучше занести деньги разработчику на развитие свободного проекта. При этом размещать Ирину можно и на своëм сервере. Кода меньше, ограничений нет. Класс.
nervix
00.00.0000 00:00+2Амазоновская алекса ждет 8 секунд, что немного удобней, но openai иногда и с этим не справляется
AnatolyBelov
00.00.0000 00:00Добрый день )
Реально работающий навык с ChatGPT по данной статье есть в Алисе?
Как вызвать ?MrFrederic
00.00.0000 00:00Есть, но Яндекс не даёт и не даст его опубликовать потому что это бы нарушило политику использования OpenAI
freylis
00.00.0000 00:00+4Благодаря вашей статье мне пришла мысль подружить мою Алису с авиасимулятором, научив Алису выполнять команды, отвлекающие непосредственно от пилотирования. Спасибо)
Piramind
00.00.0000 00:00Хорошая статья) Тоже появилась идея с навыком по заметкам взамен существующей функции.
achekalin
Почему-то подумалось: пусть ChatGPT довольно легко поймать на том, что он отвечает не по делу, а чисто "на тему вопроса" (собственно, как говорится, обратного никто и не обещал), но вот от Алисы (ну и, будем честными, от Алисы, насмотревшись на работу Siri на исходной презентации) лично я ждал, что это будет не тупо "Алиса, реши мою проблему -- Я умею только делать то, что вы точно скажете, а не проблемы решать", а именно собеседник, который будет следить за контекстом и додумывать подробности.
И вот сейчас к просто говорящей колонке, умеющей отправлять запрос в поисковик (в т.ч. и тематический, типа кинопоиска - но не умеющей среди ответов выбирать подходящий именно спрашивающему) так и не ушедшей сильно дальше описанного за годы, прикручивают именно навык быть собеседником - почему-то это сделал не Яндекс в базовую логику Алисы, а именно сторонний автор и в виде "навыка".
P.S. Иногда кажется, что компании на букву Я стало (финансово, в первую очередь - и сейчас эта составляющая окончательно стала первичной) интереснее быть посредником в вызове такси и в доставке еды, нежели оставаться лидером в поиске и делании жизнь людей лучше в широком смысле.
Krey
Так яндекс делает для Алисы свою БЯМ. Скоро будет.
root4joy
новое стоп-слово
white-wild
Саму колонку долго обкатывали прежде чем отдать на растерзание пользователям. Потребуется довольно продолжительное время, когда коммерческий продукт такого типа протестируют и включат в состав колонки.