Вводное

Часто ли нам приходится пользоваться записными книжками? Удобно ли это? Всегда ли тот кусочек бумаги, на котором записано время и место встречи, под рукой? Насколько быстро можно записать необходимую информацию? Все эти вопросы не открывают Америку, всем понятно, что цифровые планировщики гораздо практичнее, нежели традиционные письменные. Но что может быть не так с электронными записными книжками? Например, для внесения очередной записи необходимо произвести большое количество действий: от создания плана новой встречи до ручного ввода места и времени встреч в каждое отдельное поле. Казалось бы, пустяк, да и отнимает не так уж много времени. Но ведь нет предела совершенству! Как раз для улучшения данного аспекта: планирования повседневной (и не только) жизни, предназначен мой проект. Стоит отметить, что он будет полезен в основном для бэк-офиса: поможет не опоздать на важную встречу, не пропустить совещание и не забыть про дедлайн. И так, перейдем от пустых слов, непосредственно к разработке. В последующей статье описан мой опыт по разработке такого рода проекта, а я это делал впервые, поэтому сильный хейт не принимается)

Закладываем фундамент из основных знаний

В первую очередь, когда я начал разрабатывал проект, мне нужно было ознакомиться с
основными понятиями и технологиями, которые будут использоваться в проекте: подход
CRUD, фреймворк RASA (Python), конфиги Yml, база данных Postgres, которую
позже я поменял на SQLite3 из-за трудностей в работе с ней. Расскажу поподробнее, что
каждое из себя представляет:

  • CRUD
    Это подход в разработке (и не только), описывающий 4 функции, применяется в основном в базах данных. Примеры функций:

    Create – создание новой записи.
    Read – чтение существующей записи.
    Update – обновление существующей записи.
    Delete – удаление существующей записи

  • RASA
    Это открытый (и весьма популярный) фреймворк для создания чат-ботов на Python
    с использованием машинного обучения. Он состоит из двух независимых
    компонентов: Rasa NLU и Rasa Core.

    Rasa NLU (Natural Language Understanding) – модуль понимания
    естественного языка, его основной целью является преобразование ввода
    пользователя в объекты, с которыми может работать программа.

    Rasa Core – модуль, с помощью которого выстраивается основная логика и
    сценарий работы чат бота.

  • Yml
    Это язык разметки, очень похожий на XML, служит в основном для хранения,
    структуризации и передачи данных между различными сервисами, сайтами и
    площадками.

  • SQLite3
    Это легковесная, быстрая, надежная и встраиваемая система управления
    базами данных. В отличии от MySQL, PostgresSQL, Oracle и далее по списку,
    SQLite ориентирована на экономию, надежность и простоту использования.

Создание проекта: шаг за шагом

1 шаг: Подготовка RASA к работе

Мое знакомство с RASA произошло достаточно спонтанно, поэтому я изучал его "на
ощупь": сначала я написал intents (далее), записывал правила, по которым будет работать чат бот, вносил сущности в "domain.yml", написал Custom Actions и только потом сделал
шаг, который сейчас опишу, в общем, сначала сделал, а потом подумал.

Но сейчас, оглядываясь назад, гораздо логичней было бы выполнить следующие действия
первее: после создания виртуального окружения («python -m venv venv»), установки фреймворка командой «pip install rasa» и инициализации проекта командой «rasa init», необходимо произвести его первоначальную настройку, через изменение некоторых конфиг yml файлов, для дальнейшей работы. В первую очередь следует убедиться, что эти 2 строчки в файле «endpoints.yml» раскоментированы.

# This file contains the different endpoints your bot can use.

# Server where the models are pulled from.
# https://rasa.com/docs/rasa/model-storage#fetching-models-from-a-server

#models:
#  url: http://my-server.com/models/default_core@latest
#  wait_time_between_pulls:  10   # [optional](default: 100)

# Server which runs your custom actions.
# https://rasa.com/docs/rasa/custom-actions

action_endpoint:
  url: "http://localhost:5055/webhook"

2 шаг: работа с частью проекта по обработке естественного языка

На этом этапе необходимо поработать над конфигами yml в проекте: внести туда все
необходимое для обработки естественного языка: intents, entities, actions, используемые в
проекте. Первый файл, в который нужно внести изменения – «./data/nlu.yml», в нем нужно
описать 4 intents (create_plans, read_plans, update_plans, delete_plans) с примерами
(examples). Это нужно для того, чтобы ядро RASA понимало, какого типа сообщение ввел
пользователь и как на него реагировать (распознавало сущности). (i.e. чем больше
примеров для 1 intent – тем лучше, ниже показаны лишь примеры того, как они могут
выглядеть)

version: "3.1"
nlu:
#Описание намерения пользователя записать план в "ежедневник".
- intent: create_plans
  examples: |
    - Я собираюсь в [ресторан](place) в [7 вечера](time)
    - Я пойду в [ресторан](place) [2 марта](time)
    - Пойдем на [пикник](place) в [2 дня](time)

#Описание намерения пользователя прочитать планы из "ежедневника".
- intent: read_plans
  examples: |
    - Какие у меня планы на [вечер](time)?
    - Что я буду делать [завтра](time)?
    - Чем я буду занят в [пятницу](time)?

#Описание намерения пользователя обновить план в "ежедневнике".
- intent: update_plans
  examples: |
    - Я пойду в [ресторан](place) не [завтра](old_time), а
    [послезавтра](new_time).
    - Я пойду в [ресторан](place) не [2 марта](old_time), а [3
    апреля](new_time).
    - Перенеси [бар](place) с [завтра](old_time) на [послезавтра](new_time).

#Описание намерения пользователя удалить план из "ежедневника".
- intent: delete_plans
  examples: |
    - Отмени [зал](place) на [завтра](time).
    - Отмени [зал](place) на [послезавтра](time).
    - Отмени [зал](place) на [6 марта](time).

Следующий файл, который необходимо заполнить - «domain.yml», в нем необходимо
зарегистрировать все entities, intents и custom actions (над ними мы будем работать в
следующем шаге, пока представим, что их названия – action_create_row, action_read_row,
action_update_row и action_delete_row). Это делается для того, чтобы Rasa Core мог
взаимодействовать с этими объектами по ходу обработки сообщений от пользователя.

version: "3.1"
intents:
  - create_plans
  - read_plans
  - update_plans
  - delete_plans

entities:
  - time
  - place
  - old_time
  - new_time

actions:
  - action_create_plans
  - action_read_plans
  - action_update_plans
  - action_delete_plans

Третий файл, в который необходимо добавить данные – «./data/rules.yml». Здесь
необходимо заполнить правила, по которым будет действовать наш будущий бот при тех,
или иных intents.

version: "3.1"
rules:
#Описание поведения при желании пользователя записать план в "ежедневник".
- rule: Create a plan
  steps:
    - intent: create_plans
    - action: action_create_plans

#Описание поведения при желании пользователя прочитать план из "ежедневника".
- rule: Read plans
  steps:
    - intent: read_plans
    - action: action_read_plans

#Описание поведения при желании пользователя обновить план в "ежедневнике".
- rule: Update plans
  steps:
    - intent: update_plans
    - action: action_update_plans

#Описание поведения при желании пользователя удалить план в "ежедневнике".
- rule: Delete plans
  steps:
    - intent: delete_plans
    - action: action_delete_plans

3 шаг: взаимодействие с базой данных

Теперь, когда мы настроили все yml файлы, я предлагаю создать отдельный пакет "DB"
для взаимодействия с базой данных, чтобы структурировать проект и не писать один и тот
же код много раз, также, это поможет сделать проект модульным и облегчит
масштабирование. Конечно, можно не создавать ничего такого и «захардкодить» запросы к
базе данных, но согласитесь, развивать проект, у которого все запросы к базе данных
вписаны непосредственно в код, не очень удобно.))

  • queries.py – хранит в себе все SQL запросы, которые используются в проекте.

# Запрос к базе данных на вставку записи.
insert_query = """INSERT INTO plans (user_id, place, time) VALUES (?, ?,
?)"""

# Запрос к базе данных на поиск похожего времени по колонке time.
read_query = """SELECT * FROM plans WHERE time LIKE (?)"""

# Запрос к базе данных на поиск совпадающего времени и места.
check_existence_query = """SELECT * FROM plans WHERE place = (?) AND time =
(?)"""

# Запрос к базе данных на обновление колонки времени по месту и свопадающему
# старому времени.
update_query = """UPDATE plans SET time = (?) WHERE time = (?) AND place =
?"""

# Запрос к базе данных на удаление записи по совпадающему времени и месту.
delete_query = """DELETE from plans WHERE place = ? AND time = ?"""

# Запрос на создание таблицы в базе данных с определенными колонками.
create_db_query = """CREATE TABLE IF NOT EXISTS plans (user_id INTEGER, place
TEXT, time TEXT );"""
  • utils_db.py – реализовывает логику взаимодействия с базой данных:
    Создание таблицы, если таковая отсутствует, с колонками user_id (int), place
    (str), time (str).
    Добавление записи в таблицу, при условии, что все 3 колонки записи
    существуют.
    Чтение записи из таблицы, по совпадающей с пользовательским вводом
    колонкой времени.
    Обновление записи в таблице, по совпадающим с пользовательским вводом
    колонками времени и места.
    Удаление записи в таблице, по совпадающим с пользовательским вводом
    колонками времени и места.

4 шаг: завершение работы над проектом, настройка Custom Actions

Что ж, это было долго, но если вы еще здесь, то вы почти справились. Остался
заключительный этап разработки - настройка Custom Actions, довольно долгий и
кропотливый процесс, но необходимый.

Каждую Custom Action можно поделить на условные части, которые отвечают за разные
задачи. Рассмотрим action по добавлению записи в планировщик.

В первой части я создал основные методы класса CreateRow, который, кстати, необходим
для создания action, в нем метод "name" необходим для инициализации action в
фреймворке RASA, а "run" для реализации логики. Также подготовил необходимые
переменные и объекты, с помощью которых будет происходить обработка введенных
данных пользователем.

class CreateRow(Action):
  """CREATE."""
  def name(self) -> Text:
    return "action_create_plans"
  def run(self, dispatcher: CollectingDispatcher, tracker: Tracker,
      domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:
    """
    Добавление записи в ежедневник.
    """
    place = ''
    place_from_user = ''
    full_time_from_user = []
    full_time = ''
    # Анализатор, с помощью которого будет определяться начальная форма.
    morph = pymorphy2.MorphAnalyzer()
    # Сущности из последнего сообщения пользователя.
    entities = tracker.latest_message.get('entities', [])

Вторая часть включает в себя работу с entities: их извлечение, накапливание, приведение к
начальной форме с помощью модулей "pymorphy2" и "rutimeparser".

for entity in entities:
  # place_from_user - место, которое необходимо записать в
  # ежедневник в том виде, в котором его сообщил пользователь.
  # place - место, которое необходимо записать в ежедневник, после
  # приведения к начальной форме с помощью лемматизации.
  
  if entity['entity'] == 'place':
    place_from_user += entity['value']
    
    # Индекс 0 для выбора наиболее вероятного распознавания.
    place += morph.parse(entity['value'])[0].normal_form
  
  # full_time_from_user - время, которое необходимо записать в ежедневник
  # после приведения к начальной форме с помощью лемматизации.
  
  # full_time - время, которое необходимо записать в ежедневник, после
  # приведения к начальной форме с помощью лемматизации и приведения
  # к общему виду базы данных с помощью rutimeparser.
  elif entity['entity'] == 'time':
    # Индекс 0 для выбора наиболее вероятного распознавания.
    full_time_from_user.append(morph.parse(entity['value'])[0].normal_form.lower(
))
full_time_from_user = " ".join(full_time_from_user)
full_time = str(rutimeparser.parse(full_time_from_user))

В третьей части я работал с добавлением записей в базу данных, для этого использовал
ранее написанный пакет "DB", также предусмотрел неожиданные ошибки и отработал их
с помощью try-except конструкции.

try:
    query = utils_db.create_plans(0, place, full_time)

    if query:
        dispatcher.utter_message(text=f'Запись "{place_from_user}" '
                                         'записана в календарь на '
                                         f'{full_time_from_user}.')

    else:
        raise Exception

except Exception:
      dispatcher.utter_message(text='Возникла неожиданная ошибка, '
                                     'повторите попытку.')
return []

*Остальные Custom Actions работают похожим образом

Запуск проекта.

На этом разработка подошла к концу, мы выполнили все этапы, успешно настроив
фреймворк RASA (nlu и core части), написав свой пакет, что позволило структурировать
проект и разработав его собственные Custom Actions. Осталось лишь запустить наш
готовый продукт, для этого можно выполнить инструкцию из "README.md" из
репозитория проекта. После успешного запуска можно протестировать его
работоспособность:

  • Запись в планировщик

  • Чтение из планировщика

  • Обновление в планировщике

  • Удаление из планировщика

Ссылки