ProHoster > Блог > адміністрування > Створення stateful навички для Аліси на serverless функціях Яндекс.Хмари та Пітон
Створення stateful навички для Аліси на serverless функціях Яндекс.Хмари та Пітон
Почнемо з новин. Вчора Яндекс.Хмара анонсувала запуск сервісу безсерверних обчислень Yandex Cloud Functions. Це означає: ти пишеш тільки код свого сервісу (наприклад, веб-додатки або чата), а Хмара сама створює та обслуговує віртуальні машини, де він запускається, і навіть реплікує їх, якщо зростає навантаження. Думати взагалі не треба дуже зручно. І плата йде лише за час обчислень.
Втім, дехто може взагалі не платити. Це розробники зовнішніх навичок Аліси, тобто вбудованих у неї чоботів. Написати, захистити та зареєструвати таку навичку може будь-який розробник, а з сьогоднішнього дня навички навіть не треба хостити – достатньо залити їх код у хмару у вигляді тієї самої безсерверної функції.
Але є кілька нюансів. По-перше, ваш питній код може вимагати якихось залежностей, і затягувати їх у Хмару — нетривіально. По-друге, будь-якому нормальному чату потрібно зберігати десь стан діалогу (stateful тому); як зробити це в безсерверній функції найпростіше? По-третє, а як взагалі можна швидко-брудно написати навичку для Аліси чи взагалі якогось бота з ненульовим сюжетом? Про ці нюанси, власне, стаття.
Моральна підготовка
Для нетерплячих: необхідні залежності я збираю make-файлом перед завантаженням функції в хмару, стан діалогу зберігаю в яндексівському Object Storage (воно підтримує API S3), а для керування діалогом використовую власну бібліотеку tgalice. У результаті виходить такий демо-навичка. А тепер розберемо все це трохи детальніше.
Для трохи менш нетерплячих: перш ніж занурюватися в мою історію про складнощі, я дуже рекомендую почати з простого. А саме, створити однофайлову функцію-ехобота, зареєструвати та потестити її в консолі розробника Діалогів; подробиці - у блозі Діалогів и документації Хмари. І лише потім уже приступати.
Якщо хочеться відразу відчути, за що ми тут боремося, попросіть Алісу 'включи навичку «айтішний гороскоп»'. Повинен відбутися приблизно наступний діалог:
Аліса: Привіт! Ви знаходитесь у навичці «Айтішний гороскоп». Скажіть «Старт», щоб дізнатися, що обіцяють вам зірки.
Користувач: Старт
А: Будь ласка, назвіть своє ім'я
Ю: Васісуалій
А: Тепер скажіть мені рік вашого народження. Лише чотири цифри, нічого зайвого.
Ю: другий
А: Будь ласка, спробуйте ще раз. Назвіть рік вашого народження – чотири цифри.
Ю: 2002
А: Чудово! Тепер назвіть місяць вашого народження.
Ю: Лютий
А: Чудово! Нарешті, назвіть мені дату вашого народження — лише число, лише одна чи дві цифри.
Ю: 18
А: Дякую, Васісуалію! Тепер ми знаємо: вам 17 років, і ви Водолій. Оце вам, звичайно, пощастило! Зірки кажуть вам: а что говорят звёзды, вы узнаете, запустив навык.
Проблема в тому, що для підтримки навіть такого простого діалогу потрібно пам'ятати ім'я та дату народження користувача, а серед безсерверних функцій це нетривіально. Зберігати контекст оперативної пам'яті чи файликом на диску не вдасться, т.к. Яндекс.Хмара може запустити функцію на кількох віртуальних машинах одночасно і перемикатися між ними довільним чином. Прийде скористатися якимось зовнішнім сховищем. Вибрано було Object Storage, як досить недороге та нескладне сховище прямо в Яндекс.Хмарі (тобто, напевно, швидке). Як безкоштовна альтернатива можна спробувати, наприклад, халявний шматочок хмарний Монги десь далеко. І для Object Storage (він підтримує інтерфейс S3), і для Mongo є зручні пітонівські обгортки.
Інша проблема — що для ходіння і в Object Storage, і MongoDB, і в будь-яку іншу базу або сховище даних, потрібні якісь зовнішні залежності, які потрібно залити на Yandex Functions разом з кодом своєї функції. І хотілося б це робити зручно. Дуже зручно (типу як на heroku), на жаль, не вийде, але якийсь базовий комфорт можна створити, написавши скрипт для складання оточення (make-файл).
Як запустити навичку-гороскоп
Підготуватися: зайти на якусь машинку з лінуксом. В принципі, з Windows теж, напевно, можна працювати, але із запуском make-файлу тоді доведеться почаклувати. І в будь-якому випадку вам знадобиться встановлений Python не нижче 3.6.
Створити собі два бакети в Об'єкт зберігання, назвати їх будь-яким ім'ям {BUCKET NAME} и tgalice-test-cold-storage (ось це друге ім'я зараз захардшкірене в main.py мого прикладу). Перший бакет потрібен буде лише для деплою, другий — для зберігання станів діалогу.
Створити сервісний акаунтдати йому роль editor, і отримати до нього статичні креденшали {KEY ID} и {KEY VALUE} — їх використовуватимемо для запису стану діалогу. Все це потрібно, щоб функція з Я.Хмари могла отримати доступ до сховища з Я.Хмари. Колись, сподіваюся, авторизація стане автоматичною, але поки що — так.
(Не обов'язково) встановити інтерфейс командного рядкаyc. Створити функцію можна і через веб-інтерфейс, але CLI хороший тим, що всі нововведення з'являються в ньому швидше.
Тепер можна, власне, підготувати складання залежностей: запустити в командному рядку з папки з прикладом навички make all. Встановиться купа бібліотек (переважно, як завжди, непотрібних) в папку dist.
Ручками залити у Object Storage (у бакет {BUCKET NAME}) архів, що вийшов на попередньому кроці dist.zip. За бажанням, можна зробити це і з командного рядка, наприклад, використовуючи CLI AWS.
Створити безсерверну функцію через веб-інтерфейс або за допомогою утиліти yc. Для утиліти команда виглядатиме ось так:
yc serverless function version create
--function-name=horoscope
--environment=AWS_ACCESS_KEY_ID={KEY ID},AWS_SECRET_ACCESS_KEY={KEY VALUE}
--runtime=python37
--package-bucket-name={BUCKET NAME}
--package-object-name=dist.zip
--entrypoint=main.alice_handler
--memory=128M
--execution-timeout=3s
При створенні функції вручну всі параметри заповнюються аналогічно.
Тепер створену вами функцію можна тестувати через консоль розробника, а потім доопрацьовувати та публікувати навичку.
Що там під капотом
Make-файл насправді містить у собі досить простий скрипт для встановлення залежностей та їх укладання в архів dist.zip, приблизно такий:
Решта — кілька простих інструментів, загорнутих до бібліотеки tgalice. Процес заповнення даних про користувача описується конфігом form.yaml:
form_name: 'horoscope_form'
start:
regexp: 'старт|нач(ать|ни)'
suggests:
- Старт
fields:
- name: 'name'
question: Пожалуйста, назовите своё имя.
- name: 'year'
question: Теперь скажите мне год вашего рождения. Только четыре цифры, ничего лишнего.
validate_regexp: '^[0-9]{4}$'
validate_message: Пожалуйста, попробуйте ещё раз. Назовите год вашего рождения - четыре цифры.
- name: 'month'
question: Замечательно! Теперь назовите месяц вашего рождения.
options:
- январь
...
- декабрь
validate_message: То, что вы назвали, не похоже на месяц. Пожалуйста, назовите месяц вашего рождения, без других слов.
- name: 'day'
question: Отлично! Наконец, назовите мне дату вашего рождения - только число, всего одна или две цифры.
validate_regexp: '[0123]?d$'
validate_message: Пожалуйста, попробуйте ещё раз. Вам нужно назвать число своего рождения (например, двадцатое); это одна или две цифры.
Роботу з розбору цього конфігу та обчислення фінального результату бере на себе клас класу.
class CheckableFormFiller(tgalice.dialog_manager.form_filling.FormFillingDialogManager):
SIGNS = {
'январь': 'Козерог',
...
}
def handle_completed_form(self, form, user_object, ctx):
response = tgalice.dialog_manager.base.Response(
text='Спасибо, {}! Теперь мы знаем: вам {} лет, и вы {}. n'
'Вот это вам, конечно, повезло! Звёзды говорят вам: {}'.format(
form['fields']['name'],
2019 - int(form['fields']['year']),
self.SIGNS[form['fields']['month']],
random.choice(FORECASTS),
),
user_object=user_object,
)
return response
Точніше, базовий клас FormFillingDialogManager займається заповненням «форми», а метод дочірнього класу handle_completed_form каже, що робити, коли вона готова.
Крім цього основного потоку діалогу користувача треба ще привітати, а також видати довідку за командою «допомога» та випустити з навички за командою «вихід». Для цього в tgalice також є шаблон, тому ціліковий діалоговий менеджер складений зі шматочків:
CascadeDialogManager Працює просто: намагається застосувати до поточного стану діалогу всі свої складові по черзі, і вибирає першу доречну.
Як відповідь на кожне повідомлення діалоговий менеджер повертає об'єкт живлення Response, який далі можна сконвертувати в голий текст, або повідомлення в Алісі або Телеграмі - дивлячись де бот запущений; в ньому міститься і змінений стан діалогу, який потрібно зберегти. Усією цією кухнею займається ще один клас, DialogConnectorтому безпосередній скрипт для запуску навички на Yandex Functions виглядає так:
Як бачите, більшість цього коду створює підключення до S3-інтерфейсу Object Storage. Як безпосередньо використовується це підключення, можна почитати у коді tgalice.
Останній рядок створює функцію alice_handler — ту саму, яку ми наказали смикати Яндекс.Хмарі, коли задавали параметр --entrypoint=main.alice_handler.
Ось, власне, і все. Make-файли для збирання, S3-подібне Object Storage для зберігання контексту, та питоняча бібліотека tgalice. Разом з безсерверними функціями і виразністю пітона цього достатньо розробки навички здорової людини.
Ви можете запитати, навіщо потрібно створювати tgalice? Весь нудний код, що перекладає JSON'и із запиту у відповідь і зі сховища в пам'ять і назад, лежить у ній. Там лежить застосованка регулярок, функція розуміння те, що «лютий» схоже «лютий», та інше NLU для бідних. На мою думку, цього вже має бути достатньо, щоб можна було накидати прототипи навичок в yaml-файлах, не надто відволікаючись на технічні деталі.
Якщо хочеться більш серйозного NLU, можна прикрутити до своєї навички Раса або ГлибокийПавлов, але для їх налаштування будуть потрібні додаткові танці з бубном, особливо на serverless. Якщо зовсім не хочеться кодувати, варто скористатися візуальним конструктором типу Аймілогіка. Створюючи tgalice, я думав про якийсь проміжний шлях. Подивимося, що з цього вийде.