ML: Чатбот RASA: Действия
Введение
В ряде случаев отклики чатбота RASA требуют некоторого логического анализа или работы с внешними данными. Всё это можно запрограммировать на Python. Созданию таких откликов, называемых action (действие), посвящён этот документ.
Подготовка
Для работы с действиями, необходимо сделать несколько предварительных настроек. Прежде всего, в файле endpoints.yml раскомментируем следующие две линии:
action_endpoint: # endpoints.yml url: "http://localhost:5055/webhook"Затем, в разделе actions файла domain.yml перечислим используемые действия. Пока это будет единственное действие с именем action_show_time, сообщающее пользователю текущее время:
actions: # domain.yml - action_show_time
Чтобы тестирование действия было более осмысленным, в файле data/nlu.yml добавим примеры, для намерения пользователя узнать текущее время:
nlu: # data/nlu.yml - intent: what_time_is_it examples: | - Скажи мне сколько времени? - Сколько времени? - Который сейчас час? - Который час?
Для простоты, в data/rues.yml добавим жёсткое правило, обязующее бот на намерение what_time_is_it всегда вызывать действие action_show_time:
rules: # data/rues.yml - rule: Скажи мне сколько времени steps: - intent: what_time_is_it - action: action_show_timeЕстественно, действие можно вызывать и в историях наравне с откликами utter_... В отличии от простых откликов, имена откликов действий начинаются с преффикса action_...
Скрипт действия
Теперь запрограммируем собственно действие action_show_time. Для этого в actions/actions.py напишем следующий код:
from rasa_sdk import Action, Tracker from rasa_sdk.events import SlotSet from rasa_sdk.executor import CollectingDispatcher from datetime import datetime as dt from typing import Any, Text, Dict, List class ActionShowTime(Action): def name(self) -> Text: # регистрируем имя действия return "action_show_time" def run(self, dispatcher:CollectingDispatcher, tracker:Tracker, domain:Dict[Text,Any]) -> List[Dict[Text, Any]]: # при вызове действия возвращаться ответ с текущим временем: dispatcher.utter_message(text=f'Сейчас {dt.now().strftime("%H:%M")}') return []В классе ActionShowTime метод name должен возвращать имя действия, а в методе run происходит генерация отклика бота, вызовом функции utter_message. Вместо аргумента text, можно вызвать стандартный отклик, присвоив его в аргумент response (можно совмещать text и response):
dispatcher.utter_message(response="utter_goodbye")
Такой бот обучается как обычно (rasa train). Перед его тестированием в корне проекта необходимо запустить сервер RASA командой "rasa run actions". Затем запускается диалог с ботом (rasa shell или !agent.py).
Если скрипт действия меняется, повторно вызывать обучение не надо, но необходимо перезапустить сервер (rasa run actions), прервав работу предыдущего. Возникающие в скрипте ошибки видны в окне сервера. Там же видны отладочные вызовы функции print.
Усложняем действие
Метод run получает переменную tracker, из которой можно извлечь последнее сообщение от клиента. Например, перед сообщением времени, в классе ActionShowTime глубокомысленно повторим вопрос человека:
def run(self, dispatcher:CollectingDispatcher, tracker:Tracker, domain:Dict[Text,Any]) -> List[Dict[Text, Any]]: text = tracker.latest_message['text'] utter = f'На ваш вопрос "{text}" отвечу: {dt.now().strftime("%H:%M")}' dispatcher.utter_message(text=utter) return []Естественно, переменную text можно использовать с большей пользой, например, для углублённого (по сравнению с классификацией намерения) семантического анализа.
Метод run возвращает список, в который можно поместить значение слотов (ячеек памяти). Слоты должны быть перечислены в файле domain.yml, поэтому добавляем в него слот времени последнего вызова действия:
slots: LAST_TIME_ASKING: type: textПосле этого необходимо запустить повторное обучение (rasa train), иначе при попытке задать значение слота, будет возникать предупреждение о его отсутствии.
Теперь модифицируем метод ActionShowTime.run так, чтобы в слоте сохранялось (в формате dt_frm как текст) время последнего вызова (оператор return), которое (функция get_slot) будет влиять на ответ действия:
def run(self, dispatcher:CollectingDispatcher, tracker:Tracker, domain:Dict[Text,Any]) -> List[Dict[Text, Any]]: dt_frm = "%Y-%m-%d %H:%M:%S.%f" # формат даты и времени prev = tracker.get_slot("LAST_TIME_ASKING") # значение слота "LAST_TIME_ASKING" utter = f'Сейчас: {dt.now().strftime("%H:%M")}.' if prev: # если не None (уже заполнили), то: prev = dt.strptime(prev, dt_frm) # восстанавливаем время secs = (dt.now() - prev).total_seconds() # разница в секундах с текущим utter += " Об этом ты спрашивал %d секунд назад." % (secs) dispatcher.utter_message(text=utter) return [SlotSet("LAST_TIME_ASKING", dt.now().strftime(dt_frm))] # заполняем слотПример бота, сообщающего текущее время, находится в файле Bot04_Action.zip.
Dispatcher, Tracker и domain
Объект dispatcher (диспетчер), доступный в действии, при помощи метода utter_message отправляет сообщения пользователю. Он имеет следующие необязательные аргументы:
- response - utter, который необходимо вернуть пользователю.
- text - текст, возвращаемый пользователю.
- image - URL или путь к файлу картинки, которая будет послана пользователю.
- attachment - URL или путь к файлу вложения, который нужно послать пользователю.
- json_message - json-словарь, который можно послать на конкретный канал.
- buttons - меню из кнопок.
dispatcher.utter_message(buttons = [ {"payload": "/affirm", "title": "Yes"}, {"payload": "/deny", "title": "No"}, ])
Объект tracker обладает следующими свойствами
- sender_id - The unique ID of person talking to the bot.
- slots - The list of slots that can be filled as defined in the domains.
- latest_message - A dictionary containing the attributes of the latest message: intent, entities and text.
- events - A list of all previous events.
- active_loop - The name of the currently active loop.
- latest_action_name - The name of the last action the bot executed.
В словаре domain находятся все свойства из файла domain.yml.
Приведём пример получения из latest_message текста последнего сообщения, его намерения и извлечённых из него сущностей:
print(tracker.latest_message['text']) print(tracker.latest_message['intent']['name'], "[%.2f]" % (tracker.latest_message['intent']['confidence'])) if 'entities' in tracker.latest_message: for e in tracker.latest_message['entities']: print("- [%.2f] entity:%s, value:%s pos:[%d,%d]" % (e['confidence_entity'], e['entity'], e['value'], e['start'], e['end']))