ML: Чатбот RASA: Слоты и формы


Введение

Любой приличный бот должен обладать памятью. В RASA функцию памяти, по-мимо историй, выполняют слоты (slots). Они могут заполняться автоматически (при извлечении из намерения сущностей), в формах или действиях. Слоты имеют различные типы, а их совокупность называется формой (form). Например, данные о человеке (имя, возраст, пол) можно объединить в форму. Форма - это активный объект, который после запуска, стремится сам заполнить все свои слоты, настойчиво задавая человеку соответствующие вопросы.


Типы слотов

Возможные следующие типы слотов:

Используемые слоты перечисляются в разделе slots файла domain.yml:
slots:                                      # domain.yml
  TEMPERATURE:
    type: float
    min_value: -100.0                       # минимальное значение
    max_value:  100.0                       # максимальное значение
    initial_value: 0                        # начальное значение

  RISK:
    type: categorical
    values:                                 # может принимать следующие значения:
      - low
      - medium
      - high
          
  SHOPPING_ITEMS:
    type: any    
    influence_conversation: false           # не влияет на логику диалогов

Если у слота установлено свойство "influence_conversation: true" (по умолчанию), то такие слоты могут влиять на поток диалога в историях (при помощи разделов slot_was_set). Если же значение этого свойства false, то слоты только хранят информацию и не влияют на диалоги. При желании можно также создавать кастомизированные типы слотов.

Существует мнение, что для слотов, которые влияют на истории (influence_conversation: true), не стоит устанавливать начальное значение (initial_value). RASA при обучении видит установленное всюду значение слота, и думает, что это "нормальный" случай, который должен быть всегда, что искажает обучение.

Важно не забывать, что slot_was_set в историях и правилах считает одинаковыми текстовые слоты с различными значениями. Если нужны подобные конкретизации, следует использовать категориальные слоты.


Заполнение слотов

По умолчанию, если имя слота (slot) и имя сущности (entity) совпадают, то слот заполняется значением при извлечении из текста одноименной сущности. Так, пусть в domain.yml определено:

entities:                                   # domain.yml
  - PERSON

slots:
  PERSON:
    type: text
В data/nlu.yml приведём примеры для намерения пользователя сообщить своё имя:
- intent: my_name                           # data/nlu.yml
  examples: |
    - меня зовут [Настя](PERSON)
    - меня зовут [Анастасия](PERSON)
    - меня зовут [Тимур](PERSON)
    - [Маша](PERSON)

При распознании этого намерения, будет извлечена сущность PERSON и помещена в одноименный слот PERSON, который можно использовать, например, в откликах:

                    
responses:                                  # domain.yml
  utter_glad_to_meet_you:
  - text: "Рад с тобой познакомиться {PERSON}."
Пример такого бота находится в проекте Bot02_PERSON.zip.


Для сложных ботов, одна и та-же сущность (entity) может встречаться в различных намерениях (intent). Поэтому автоматическое отождествление имени слота (slot) и имени сущности не всегда удобно. Чтобы запретить подобное отождествление, в файле domain.yml необходимо добавить строку:

config:                                     # domain.yml
  store_entities_as_slots: false            # ни одна сущность не свяжется со слотом
Если же отождествление необходимо запретить только для одного слота, это делается в списке слотов (свойство auto_fill: false):
slots:                                      # domain.yml
  ORDER:                                    # имя слота
    type: text                              # тип слота
    auto_fill: false                        # автоматически не заполняется


Формы

Сущности и слоты с различными названиями (или свойством auto_fill: false) можно связать при помощи форм. Их размещают в файле domain.yml. Имя формы одновременно является именем действия, которое можно использовать в историях (story) или правилах (rule). Когда форма с несколькими слотами активирована, RASA начинает повторять вопросы, пока все слоты формы не будут заполнены. Как и для правил, чтобы формы в циклах работали, в "config.yml" надо указать:

policies:                                   # config.yml
- name: RulePolicy

Пусть есть форма с именем form_person_info и двумя слотами (именем и возрастом):

forms:                                   
   form_person_info:                        # имя формы   
      required_slots:                       # для полного заполнения требуются такие слоты:
         NAME:                              # имя слота
            - type: from_entity             # заполняется из сущности,
              entity: PERSON                # которая имеет имя PERSON
         AGE:                               # имя слота
            - type: from_entity             # заполняется из сущности,
              entity: NUMBER                # которая имеет имя NUMBER
              not_intent:                   # где игнорировать заполнение слота
              - number_of_items           
Для формирования соответствующих вопросов необходимо в domain.yml определить отклики бота. Они начинаются с префикса utter_ask_... Далее идёт _<имя формы>_<имя слота> (имя формы можно опустить). В нашем случае это могут быть отклики: utter_ask_form_person_info_NAME и utter_ask_form_person_info_AGE. В раздел entities, как обычно, добавляем PERSON, NUMBER, а в data/nlu.yml прописываем достаточное число примеров для этих сущностей.


Активация формы

Чтобы форма запустилась, в историях напишем пример диалога в котором, при активации формы, бот "проваливается" в заполнение её слотов:
stories:                                  # data/stories.yml  
- story: 1. привет, выяснение имени и возраста с удачным прохождением
  steps:
  - intent:      greet                    # 🙎 привет
  - action:      utter_greet              # 💻 Рад тебе
  
  - action:      form_person_info         # активируем форму 
  - active_loop: form_person_info         # идёт событие Loop(form_person_info)
  
  - active_loop: null                     # форма заполнена, нет активных форм  
  - slot_was_set:                         # были определены все  слоты
    - NAME 
    - AGE
  - action:      utter_you_NAME_AGE       # 💻 Тебя зовут Настя и тебе 16 лет (в конце)

Разберём теперь ход истории.

Полезно проанализировать события, возникающие при заполнении этой формы (команда "=e" в !agent.py):

- intent: greet [1.00]
    привет
- action: utter_greet                       < [1.00] policy_0_MemoizationPolicy
    Как я рад тебя видеть!
- action: form_person_info                  < [1.00] policy_0_MemoizationPolicy
- action: Loop(form_person_info)
- action: SlotSet(key: requested_slot, value: NAME)
    Как тебя зовут?
- intent: my_name [1.00]
    Маша
        - PERSON         [1.00] pos:[ 0, 4] value: Маша
- action: form_person_info                  < [1.00] policy_1_RulePolicy
- action: SlotSet(key: NAME, value: Маша)
- action: SlotSet(key: requested_slot, value: AGE)
    Сколько тебе лет?
- intent: my_age [1.00]
    16
        - NUMBER         [1.00] pos:[ 0, 2] value: 16
- action: form_person_info                  < [1.00] policy_1_RulePolicy
- action: SlotSet(key: AGE, value: 16)
- action: SlotSet(key: requested_slot, value: None)
- action: Loop(None)
- action: utter_you_NAME_AGE                < [0.90] policy_2_TEDPolicy
    Тебя зовут Маша и тебе 16 лет.
В принципе, форма запустится и корректно отработает, если в истории убрать событие active_loop: form_person_info. Он в любом случае будет активировано RASA. Однако, наличие его в обучающей последовательности полезно. Обратим внимание на политику policy_1_RulePolicy которая применяется при активации формы. Аналогично другим правилам (data/rules.yml), запуск формы не "портит" истории, что-бы не происходило внутри заполнения формы (это как бы отдельная процедура).


Несчастливые пути

Иногда пользователь при заполнении формы начинает нести бред или выявляет желание прервать заполнение слотов, например, таким намерением:

nlu:                                        # data/nlu.yml
  - intent: irritation 
    examples: |   
    - что ты достал со своими вопросами
    - отвали
    - отстань
Добавим истории, которые обрабатывают эту ситуацию. Первая история при появления намерения irritation переспрашивает, и получив намерение продолжить, продолжает заполнение слотов:
- story: 2. привет, выяснение имени и возраста с прерыванием
  steps:
  - intent:      greet                    # 🙎 привет
  - action:      utter_greet              # 💻 Рад тебе
  
  - action:      form_person_info         # активируем форму и запускаем цикл  
  - active_loop: form_person_info         # форма сейчас активна (заполняется)  
  
  - intent:      irritation               # 🙎 отвали
  - action:      utter_not_understand     # 💻 Не понял тебя. Продолжим?
  - intent:      affirm                   # 🙎 да
    
  - action:      form_person_info         # активируем форму     
  - active_loop: form_person_info         # форма сейчас активна (заполняется)  
  
  - active_loop: null                     # форма заполнена, нет активных форм
  - slot_was_set:                         # были определены все  слоты
    - NAME 
    - AGE
  - action:      utter_you_NAME_AGE       # 💻 Тебя зовут Настя и тебе 16 лет (в конце)  
Вторая история прекращает петлю заполнения формы вызовом действия action_deactivate_loop:
- story: 3. привет, выяснение имени и возраста с прерыванием
  steps:
  - intent:      greet                    # 🙎 привет
  - action:      utter_greet              # 💻 Рад тебе
  
  - action:      form_person_info         # активируем форму и запускаем цикл  
  - active_loop: form_person_info         # форма сейчас активна (заполняется)  
  
  - intent:      irritation               # 🙎 отвали
  - action:      utter_not_understand     # 💻 Не понял тебя. Продолжим?
  - intent:      deny                     # 🙎 нет    
  - action:      action_deactivate_loop   # насильно прекращаем форму
  - active_loop: null
  - action:      utter_goodbye            # До скорой встречи

Можно также обрабатывать прерывания заполнения формы в правилах:

rules:
- rule: Example of an unhappy path
  condition:  
  - active_loop: restaurant_form           # Условие - форма активан
  steps:  
  - intent: chitchat                       # Несчастливое прерывание болтовней `chitchat`.
  - action: utter_chitchat  
  - action: restaurant_form                # Возвращаемся к заполнению формы
  - active_loop: restaurant_form
  


Тестирования формы без прерывания

Для тестирования заполнения формы в ситуации, когда человек "послушно" отвечает, в tests/test_stories.yml создадим следующий пример истории:
stories:                                    # tests/test_stories.yml

- story: 1. привет, выяснение имени и возраста с удачным прохождением
  steps:
    - user: |                               # 🙎 привет
        привет                           
      intent:      greet                 
    - action:      utter_greet              # 💻 Рад тебе    
    - action:      form_person_info         # должна активироваться форма
    - active_loop: form_person_info         # и войти в цикл        
    
    - user: |                               # 🙎 Настя
        [Настя](PERSON)      
      intent:      my_name  
    - action:      form_person_info
    - active_loop: form_person_info         # продолжить цикл        
    
    - user: |                               # 🙎 16
        [16](NUMBER)         
      intent:      my_age          
    - action:      form_person_info      
    - active_loop: null                     # должна деактивироваться            
    - action:      utter_you_NAME_AGE       # 💻 Тебя зовут Настя и тебе 16 лет (в конце)

Тестирования формы с прерыванием

Пусть человек начинает вести себя непослушно в самом начале диалога. Это соответствует следующему тесту:
- story: 2. привет, выяснение имени и возраста с прерыванием в начале
  steps:
    - user: |                               # 🙎 привет
        привет                           
      intent:      greet                 
    - action:      utter_greet              # 💻 Рад тебе    
    - action:      form_person_info         # должна активироваться форма
    - active_loop: form_person_info         # форма сейчас активна (заполняется)    
    
    - user: |                               # 🙎 отвали 
        отвали      
      intent:      irritation  
    - action:      utter_not_understand     # 💻 Не понял тебя. Продолжим?

    - user: |                               # 🙎 да
        ну давай          
      intent:      affirm                   # 🙎 ну давай             
    - action:      form_person_info         # должна активироваться форма
    - active_loop: form_person_info         # и снова войти в цикл        
                  
    - user: |                               # 🙎 Настя
        [Настя](PERSON)      
      intent:      my_name  
    - action:      form_person_info
    - active_loop: form_person_info         # продолжить цикл        
    
    - user: |                               # 🙎 16
        [16](NUMBER)         
      intent:      my_age          
    - action:      form_person_info      
    - active_loop: null                     # должна деактивироваться             
    - action:      utter_you_NAME_AGE       # 💻 Тебя зовут Настя и тебе 16 лет (в конце)
Аналогично, человек мог попытаться прервать заполнение формы в её середине. Для этого напишем ещё один тест:
- story: 3. привет, выяснение имени и возраста с прерыванием в середине
  steps:
    - user: |                               # 🙎 привет
        привет                           
      intent:      greet                 
    - action:      utter_greet              # 💻 Рад тебе    

    - action:      form_person_info         # должна активироваться форма
    - active_loop: form_person_info         # форма сейчас активна (заполняется)    
                      
    - user: |                               # 🙎 Настя
        [Настя](PERSON)      
      intent:      my_name  
    - action:      form_person_info
    - active_loop: form_person_info         # продолжить цикл        
    
    - user: |                               # 🙎 отвали
        отвали      
      intent:      irritation  
    - action:      utter_not_understand     # 💻 Не понял тебя. Продолжим?
    - user: |                               # 🙎 да
        ну давай          
      intent:      affirm                   # 🙎 ну давай       
      
    - action:      form_person_info         # должна активироваться форма
    - active_loop: form_person_info         # и снова войти в цикл        
        
    - user: |                               # 🙎 16
        [16](NUMBER)         
      intent:      my_age          
    - action:      form_person_info      
    - active_loop: null                     # должна деактивироваться
             
    - action:      utter_you_NAME_AGE       # 💻 Тебя зовут Настя и тебе 16 лет (в конце)