Стварэнне 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. Пры жаданні, можна зрабіць гэта і з каманднага радка, напрыклад, выкарыстоўваючы AWS CLI.
  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, можна прыкруціць да свайго навыку Раса або DeepPavlov, Але для іх наладкі запатрабуюцца дадатковыя скокі з бубнам, асабліва на serverless. Калі зусім не жадаецца кадзіць, варта скарыстацца візуальным канструктарам тыпу Aimylogic. Ствараючы tgalice, я думаў аб нейкім прамежкавым шляху. Пабачым, што з гэтага выйдзе.

Ну а сягоння ўступайце ў чат распрацоўшчыкаў алісяіх навыкаў, чытайце дакументацыю, і стварайце выдатныя навыкі!

Крыніца: habr.com

Дадаць каментар