Створення stateful навички для Аліси на serverless функціях Яндекс.Хмари та Пітон

Почнемо з новин. Вчора Яндекс.Хмара анонсувала запуск сервісу безсерверних обчислень Yandex Cloud Functions. Це означає: ти пишеш тільки код свого сервісу (наприклад, веб-додатки або чата), а Хмара сама створює та обслуговує віртуальні машини, де він запускається, і навіть реплікує їх, якщо зростає навантаження. Думати взагалі не треба дуже зручно. І плата йде лише за час обчислень.

Втім, дехто може взагалі не платити. Це розробники зовнішніх навичок Аліси, тобто вбудованих у неї чоботів. Написати, захистити та зареєструвати таку навичку може будь-який розробник, а з сьогоднішнього дня навички навіть не треба хостити – достатньо залити їх код у хмару у вигляді тієї самої безсерверної функції.

Але є кілька нюансів. По-перше, ваш питній код може вимагати якихось залежностей, і затягувати їх у Хмару — нетривіально. По-друге, будь-якому нормальному чату потрібно зберігати десь стан діалогу (stateful тому); як зробити це в безсерверній функції найпростіше? По-третє, а як взагалі можна швидко-брудно написати навичку для Аліси чи взагалі якогось бота з ненульовим сюжетом? Про ці нюанси, власне, стаття.

Створення stateful навички для Аліси на serverless функціях Яндекс.Хмари та Пітон

Моральна підготовка

Для нетерплячих: необхідні залежності я збираю make-файлом перед завантаженням функції в хмару, стан діалогу зберігаю в яндексівському Object Storage (воно підтримує API S3), а для керування діалогом використовую власну бібліотеку tgalice. У результаті виходить такий демо-навичка. А тепер розберемо все це трохи детальніше.

Для трохи менш нетерплячих: перш ніж занурюватися в мою історію про складнощі, я дуже рекомендую почати з простого. А саме, створити однофайлову функцію-ехобота, зареєструвати та потестити її в консолі розробника Діалогів; подробиці - у блозі Діалогів и документації Хмари. І лише потім уже приступати.

Якщо хочеться відразу відчути, за що ми тут боремося, попросіть Алісу 'включи навичку «айтішний гороскоп»'. Повинен відбутися приблизно наступний діалог:

Аліса: Привіт! Ви знаходитесь у навичці «Айтішний гороскоп». Скажіть «Старт», щоб дізнатися, що обіцяють вам зірки.
Користувач: Старт
А: Будь ласка, назвіть своє ім'я
Ю: Васісуалій
А: Тепер скажіть мені рік вашого народження. Лише чотири цифри, нічого зайвого.
Ю: другий
А: Будь ласка, спробуйте ще раз. Назвіть рік вашого народження – чотири цифри.
Ю: 2002
А: Чудово! Тепер назвіть місяць вашого народження.
Ю: Лютий
А: Чудово! Нарешті, назвіть мені дату вашого народження — лише число, лише одна чи дві цифри.
Ю: 18
А: Дякую, Васісуалію! Тепер ми знаємо: вам 17 років, і ви Водолій. Оце вам, звичайно, пощастило! Зірки кажуть вам: а что говорят звёзды, вы узнаете, запустив навык.

Проблема в тому, що для підтримки навіть такого простого діалогу потрібно пам'ятати ім'я та дату народження користувача, а серед безсерверних функцій це нетривіально. Зберігати контекст оперативної пам'яті чи файликом на диску не вдасться, т.к. Яндекс.Хмара може запустити функцію на кількох віртуальних машинах одночасно і перемикатися між ними довільним чином. Прийде скористатися якимось зовнішнім сховищем. Вибрано було Object Storage, як досить недороге та нескладне сховище прямо в Яндекс.Хмарі (тобто, напевно, швидке). Як безкоштовна альтернатива можна спробувати, наприклад, халявний шматочок хмарний Монги десь далеко. І для Object Storage (він підтримує інтерфейс S3), і для Mongo є зручні пітонівські обгортки.

Інша проблема — що для ходіння і в Object Storage, і MongoDB, і в будь-яку іншу базу або сховище даних, потрібні якісь зовнішні залежності, які потрібно залити на Yandex Functions разом з кодом своєї функції. І хотілося б це робити зручно. Дуже зручно (типу як на heroku), на жаль, не вийде, але якийсь базовий комфорт можна створити, написавши скрипт для складання оточення (make-файл).

Як запустити навичку-гороскоп

  1. Підготуватися: зайти на якусь машинку з лінуксом. В принципі, з Windows теж, напевно, можна працювати, але із запуском make-файлу тоді доведеться почаклувати. І в будь-якому випадку вам знадобиться встановлений Python не нижче 3.6.
  2. Схиляти собі з гітхабу приклад гороскопної навички.
  3. Зареєструватися в Я.Облаці: https://cloud.yandex.ru
  4. Створити собі два бакети в Об'єкт зберігання, назвати їх будь-яким ім'ям {BUCKET NAME} и tgalice-test-cold-storage (ось це друге ім'я зараз захардшкірене в main.py мого прикладу). Перший бакет потрібен буде лише для деплою, другий — для зберігання станів діалогу.
  5. Створити сервісний акаунтдати йому роль editor, і отримати до нього статичні креденшали {KEY ID} и {KEY VALUE} — їх використовуватимемо для запису стану діалогу. Все це потрібно, щоб функція з Я.Хмари могла отримати доступ до сховища з Я.Хмари. Колись, сподіваюся, авторизація стане автоматичною, але поки що — так.
  6. (Не обов'язково) встановити інтерфейс командного рядка yc. Створити функцію можна і через веб-інтерфейс, але CLI хороший тим, що всі нововведення з'являються в ньому швидше.
  7. Тепер можна, власне, підготувати складання залежностей: запустити в командному рядку з папки з прикладом навички make all. Встановиться купа бібліотек (переважно, як завжди, непотрібних) в папку dist.
  8. Ручками залити у Object Storage (у бакет {BUCKET NAME}) архів, що вийшов на попередньому кроці dist.zip. За бажанням, можна зробити це і з командного рядка, наприклад, використовуючи CLI AWS.
  9. Створити безсерверну функцію через веб-інтерфейс або за допомогою утиліти 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

При створенні функції вручну всі параметри заповнюються аналогічно.

Тепер створену вами функцію можна тестувати через консоль розробника, а потім доопрацьовувати та публікувати навичку.

Створення stateful навички для Аліси на serverless функціях Яндекс.Хмари та Пітон

Що там під капотом

Make-файл насправді містить у собі досить простий скрипт для встановлення залежностей та їх укладання в архів dist.zip, приблизно такий:

mkdir -p dist/
pip3 install -r requirements.txt --target dist/ 
cp main.py dist/main.py
cp form.yaml dist/form.yaml
cd dist && zip --exclude '*.pyc' -r ../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 також є шаблон, тому ціліковий діалоговий менеджер складений зі шматочків:

dm = tgalice.dialog_manager.CascadeDialogManager(
    tgalice.dialog_manager.GreetAndHelpDialogManager(
        greeting_message=DEFAULT_MESSAGE,
        help_message=DEFAULT_MESSAGE,
        exit_message='До свидания, приходите в навык "Айтишный гороскоп" ещё!'
    ),
    CheckableFormFiller(`form.yaml`, default_message=DEFAULT_MESSAGE)
)

CascadeDialogManager Працює просто: намагається застосувати до поточного стану діалогу всі свої складові по черзі, і вибирає першу доречну.

Як відповідь на кожне повідомлення діалоговий менеджер повертає об'єкт живлення Response, який далі можна сконвертувати в голий текст, або повідомлення в Алісі або Телеграмі - дивлячись де бот запущений; в ньому міститься і змінений стан діалогу, який потрібно зберегти. Усією цією кухнею займається ще один клас, DialogConnectorтому безпосередній скрипт для запуску навички на Yandex Functions виглядає так:

...
session = boto3.session.Session()
s3 = session.client(
    service_name='s3',
    endpoint_url='https://storage.yandexcloud.net',
    aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
    aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
    region_name='ru-central1',
)
storage = tgalice.session_storage.S3BasedStorage(s3_client=s3, bucket_name='tgalice-test-cold-storage')
connector = tgalice.dialog_connector.DialogConnector(dialog_manager=dm, storage=storage)
alice_handler = connector.serverless_alice_handler

Як бачите, більшість цього коду створює підключення до S3-інтерфейсу Object Storage. Як безпосередньо використовується це підключення, можна почитати у коді tgalice.
Останній рядок створює функцію alice_handler — ту саму, яку ми наказали смикати Яндекс.Хмарі, коли задавали параметр --entrypoint=main.alice_handler.

Ось, власне, і все. Make-файли для збирання, S3-подібне Object Storage для зберігання контексту, та питоняча бібліотека tgalice. Разом з безсерверними функціями і виразністю пітона цього достатньо розробки навички здорової людини.

Ви можете запитати, навіщо потрібно створювати tgalice? Весь нудний код, що перекладає JSON'и із запиту у відповідь і зі сховища в пам'ять і назад, лежить у ній. Там лежить застосованка регулярок, функція розуміння те, що «лютий» схоже «лютий», та інше NLU для бідних. На мою думку, цього вже має бути достатньо, щоб можна було накидати прототипи навичок в yaml-файлах, не надто відволікаючись на технічні деталі.

Якщо хочеться більш серйозного NLU, можна прикрутити до своєї навички Раса або ГлибокийПавлов, але для їх налаштування будуть потрібні додаткові танці з бубном, особливо на serverless. Якщо зовсім не хочеться кодувати, варто скористатися візуальним конструктором типу Аймілогіка. Створюючи tgalice, я думав про якийсь проміжний шлях. Подивимося, що з цього вийде.

Ну а нині вступайте у чат розробників алисьих навичок, читайте документацію, і створюйте чудові навички!

Джерело: habr.com

Додати коментар або відгук