diff --git a/.gitignore b/.gitignore index 7fb4febf38f9..9ea6de6fb31f 100644 --- a/.gitignore +++ b/.gitignore @@ -80,7 +80,11 @@ failed_stories.md errors.json pip-wheel-metadata/* events.db +events.db-shm +events.db-wal rasa.db +rasa.db-shm +rasa.db-wal *.swp *.coverage* env diff --git a/changelog/5189.doc.rst b/changelog/5189.doc.rst new file mode 100644 index 000000000000..4d7f55483574 --- /dev/null +++ b/changelog/5189.doc.rst @@ -0,0 +1 @@ +Added user guide for reminders and external events, including ``reminderbot`` demo. \ No newline at end of file diff --git a/changelog/5189.misc.rst b/changelog/5189.misc.rst new file mode 100644 index 000000000000..3f4edbdd1d5f --- /dev/null +++ b/changelog/5189.misc.rst @@ -0,0 +1 @@ +Added `db-shm` and `db-wal` files to `.gitignore` \ No newline at end of file diff --git a/docs/core/reminders-and-external-events.rst b/docs/core/reminders-and-external-events.rst new file mode 100644 index 000000000000..7c736306d05c --- /dev/null +++ b/docs/core/reminders-and-external-events.rst @@ -0,0 +1,169 @@ +:desc: Learn how to use external events and schedule reminders. + +.. _reminders-and-external-events: + +Reminders and External Events +============================= + +.. edit-link:: + +The ``ReminderScheduled`` event and the +`trigger_intent endpoint <../../api/http-api/#operation/triggerConversationIntent>`_ let your assistant remind you +about things after a given period of time, or to respond to external events (other applications, sensors, etc.). +You can find a full example assistant that implements these features +`here `_. + +.. contents:: + :local: + +.. _reminders: + +Reminders +--------- + +Instead of an external sensor, you might just want to be reminded about something after a certain amount of time. +For this, Rasa provides the special event ``ReminderScheduled``, and another event, ``ReminderCancelled``, to unschedule a reminder. + +.. _scheduling-reminders-guide: + +Scheduling Reminders +^^^^^^^^^^^^^^^^^^^^ + +Let's say you want your assistant to remind you to call a friend in 5 seconds. +(You probably want some longer time span, but for the sake of testing, let it be 5 seconds.) +Thus, we define an intent ``ask_remind_call`` with some NLU data, + +.. code-block:: md + + ## intent:ask_remind_call + - remind me to call [Albert](name) + - remind me to call [Susan](name) + - later I have to call [Daksh](name) + - later I have to call [Anna](name) + ... + +and connect this intent with a new custom action ``action_set_reminder``. +We could make this connection by providing training stories (recommended for more complex assistants), or using the :ref:`mapping-policy`. + +The custom action ``action_set_reminder`` should schedule a reminder that, 5 seconds later, triggers an intent ``EXTERNAL_reminder`` with all the entities that the user provided in his/her last message (similar to an external event): + +.. literalinclude:: ../../examples/reminderbot/actions.py + :pyobject: ActionSetReminder + +Note that this requires the ``datetime`` and ``rasa_sdk.events`` packages. + +Finally, we define another custom action ``action_react_to_reminder`` and link it to the ``EXTERNAL_reminder`` intent: + +.. code-block:: md + + - EXTERNAL_reminder: + triggers: action_react_to_reminder + +where the ``action_react_to_reminder`` is + +.. literalinclude:: ../../examples/reminderbot/actions.py + :pyobject: ActionReactToReminder + +Instead of a custom action, we could also have used a simple response template. +But here we want to make use of the fact that the reminder can carry entities, and we can process the entities in this custom action. + +.. warning:: + + Reminders are cancelled whenever you shutdown your Rasa server. + +.. warning:: + + Reminders currently (Rasa 1.8) don't work in `rasa shell`. + You have to test them with a + `running Rasa X server `_ instead. + +.. note:: + + Proactively reaching out to the user is dependent on the abilities of a channel and + hence not supported by every channel. If your channel does not support it, consider + using the :ref:`callbackInput` channel to send messages to a `webhook `_. + +.. _cancelling-reminders-guide: + +Cancelling Reminders +^^^^^^^^^^^^^^^^^^^^ + +Sometimes the user may want to cancel a reminder that he has scheduled earlier. +A simple way of adding this functionality to your assistant is to create an intent ``ask_forget_reminders`` and let your assistant respond to it with a custom action such as + +.. literalinclude:: ../../examples/reminderbot/actions.py + :pyobject: ForgetReminders + +Here, ``ReminderCancelled()`` simply cancels all the reminders that are currently scheduled. +Alternatively, you may provide some parameters to narrow down the types of reminders that you want to cancel. +For example, + + - ``ReminderCancelled(intent="greet")`` cancels all reminders with intent ``greet`` + - ``ReminderCancelled(entities={...})`` cancels all reminders with the given entities + - ``ReminderCancelled("...")`` cancels the one unique reminder with the given name "``...``" that you supplied + during its creation + +.. _external-event-guide: + +External Events +--------------- + +Let's say you want to send a message from some other device to change the course of an ongoing conversation. +For example, some moisture-sensor attached to a Raspberry Pi should inform your personal assistant that your favourite +plant needs watering, and your assistant should then relay this message to you. + +To do this, your Raspberry Pi needs to send a message to the `trigger_intent endpoint <../../api/http-api/#operation/triggerConversationIntent>`_ of your conversation. +As the name says, this injects a user intent (possibly with entities) into your conversation. +So for Rasa it is almost as if you had entered a message that got classified with this intent and these entities. +Rasa then needs to respond to this input with an action such as ``action_warn_dry``. +The easiest and most reliable way to connect this action with the intent is via the :ref:`mapping-policy`. + +.. _getting-conversation-id: + +Getting the Conversation ID +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The first thing we need is the Session ID of the conversation that your sensor should send a notification to. +An easy way to get this is to define a custom action (see :ref:`custom-actions`) that displays the ID in the conversation. +For example: + +.. literalinclude:: ../../examples/reminderbot/actions.py + :pyobject: ActionTellID + +In addition, we also declare an intent ``ask_id``, define some NLU data for it, and add both ``action_tell_id`` and +``ask_id`` to the domain file, where we specify that one should trigger the other: + +.. code-block:: md + + intents: + - ask_id: + triggers: action_tell_id + +Now, when you ask "What is the ID of this conversation?", the assistant replies with something like "The ID of this +conversation is: 38cc25d7e23e4dde800353751b7c2d3e". + +If you want your assistant to link to the Raspberry Pi automatically, you will have to write a custom action that +informs the Pi about the conversation id when your conversation starts (see :ref:`custom_session_start`). + +.. _responding_to_external_events: + +Responding to External Events +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now that we have our Session ID, we need to prepare the assistant so it responds to messages from the sensor. +To this end, we define a new intent ``EXTERNAL_dry_plant`` without any NLU data. +This intent will later be triggered by the external sensor. +Here, we start the intent name with ``EXTERNAL_`` to indicate that this is not something the user would say, but you can name the intent however you like. + +In the domain file, we now connect the intent ``EXTERNAL_dry_plant`` with another custom action ``action_warn_dry``, e.g. + +.. literalinclude:: ../../examples/reminderbot/actions.py + :pyobject: ActionWarnDry + +Now, when you are in a conversation with id ``38cc25d7e23e4dde800353751b7c2d3e``, then running + +.. code-block:: shell + + curl -H "Content-Type: application/json" -X POST -d '{"name": "EXTERNAL_dry_plant", "entities": {"plant": "Orchid"}}' http://localhost:5005/conversations/38cc25d7e23e4dde800353751b7c2d3e/trigger_intent + +in the terminal will cause your assistant to say "Your Orchid needs some water!". diff --git a/docs/core/responses.rst b/docs/core/responses.rst index 2342c503eac7..b51b0c2129eb 100644 --- a/docs/core/responses.rst +++ b/docs/core/responses.rst @@ -166,22 +166,5 @@ Proactively Reaching Out to the User with External Events You may want to proactively reach out to the user, for example to display the output of a long running background operation or notify the user of an external event. - -To do so, you can ``POST`` an intent to the -`trigger_intent endpoint <../../api/http-api/#operation/triggerConversationIntent>`_. -The intent, let's call it ``EXTERNAL_sensor``, will be treated as if the user had sent a message with this intent. -You can even provide a dictionary of entities as parameters, e.g. ``{"temperature": "high"}``. -For your bot to respond, we recommend you use the :ref:`mapping-policy` to connect the sent intent ``EXTERNAL_sensor`` -with the action you want your bot to execute, e.g. ``utter_warn_temperature``. -You can also use a custom action here, of course. - -Use the ``output_channel`` query parameter to specify which output -channel should be used to communicate the assistant's responses back to the user. -Any messages that are dispatched in the custom action will be forwarded to the specified output channel. -Set this parameter to ``"latest"`` if you want to use the latest input channel that the user has used. - -.. note:: - - Proactively reaching out to the user is dependent on the abilities of a channel and - hence not supported by every channel. If your channel does not support it, consider - using the :ref:`callbackInput` channel to send messages to a webhook. +To learn more, check out `reminderbot `_ in +the Rasa examples directory, or the `docs page on this topic <../../core/external-events-and-reminders>`_. diff --git a/docs/index.rst b/docs/index.rst index 90a42982aed4..28519e449532 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,7 @@ Understand messages, hold conversations, and connect to messaging channels and A core/domains core/responses core/actions + core/reminders-and-external-events core/policies core/slots core/forms diff --git a/examples/reminderbot/README.md b/examples/reminderbot/README.md new file mode 100644 index 000000000000..3acba395cd04 --- /dev/null +++ b/examples/reminderbot/README.md @@ -0,0 +1,40 @@ +# Reminderbot + +The `reminderbot` example demonstrates how your bot can respond to external events or reminders. + +## What’s inside this example? + +This example contains some training data and the main files needed to build an +assistant on your local machine. The `reminderbot` consists of the following files: + +- **data/nlu.md** contains training examples for the NLU model +- **data/stories.md** contains training stories for the Core model +- **config.yml** contains the model configuration +- **domain.yml** contains the domain of the assistant +- **credentials.yml** contains credentials for the different channels +- **endpoints.yml** contains the different endpoints reminderbot can use +- **actions.py** contains the custom actions that deal with external events and reminders + +## How to use this example? + +To train and chat with `reminderbot`, execute the following steps: + +1. Train a Rasa Open Source model containing the Rasa NLU and Rasa Core models by running: + ``` + rasa train + ``` + The model will be stored in the `/models` directory as a zipped file. + +2. Run a Rasa action server with + ``` + rasa run actions + ``` + +3. Run a Rasa X to talk to your bot. + If you don't have a Rasa X server running, you can test things with `rasa x` in a separate shell (the action server must keep running). + +For more information about the individual commands, please check out our +[documentation](http://rasa.com/docs/rasa/user-guide/command-line-interface/). + +## Encountered any issues? +Let us know about it by posting on [Rasa Community Forum](https://forum.rasa.com)! diff --git a/examples/reminderbot/__init__.py b/examples/reminderbot/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/examples/reminderbot/actions.py b/examples/reminderbot/actions.py new file mode 100644 index 000000000000..97562853db55 --- /dev/null +++ b/examples/reminderbot/actions.py @@ -0,0 +1,127 @@ +# This files contains your custom actions which can be used to run +# custom Python code. +# +# See this guide on how to implement these action: +# https://rasa.com/docs/rasa/core/actions/#custom-actions/ + + +# This is a simple example for an assistant that schedules reminders and +# reacts to external events. + +from typing import Any, Text, Dict, List +import datetime + +from rasa_sdk import Action, Tracker +from rasa_sdk.events import ReminderScheduled, ReminderCancelled +from rasa_sdk.executor import CollectingDispatcher + + +class ActionSetReminder(Action): + """Schedules a reminder, supplied with the last message's entities.""" + + def name(self) -> Text: + return "action_set_reminder" + + async def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + + dispatcher.utter_message("I will remind you in 5 seconds.") + + date = datetime.datetime.now() + datetime.timedelta(seconds=5) + entities = tracker.latest_message.get("entities") + + reminder = ReminderScheduled( + "EXTERNAL_reminder", + trigger_date_time=date, + entities=entities, + name="my_reminder", + kill_on_user_message=False, + ) + + return [reminder] + + +class ActionReactToReminder(Action): + """Reminds the user to call someone.""" + + def name(self) -> Text: + return "action_react_to_reminder" + + async def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + + name = next(tracker.get_latest_entity_values("name"), "someone") + dispatcher.utter_message(f"Remember to call {name}!") + + return [] + + +class ActionTellID(Action): + """Informs the user about the conversation ID.""" + + def name(self) -> Text: + return "action_tell_id" + + async def run( + self, dispatcher, tracker: Tracker, domain: Dict[Text, Any] + ) -> List[Dict[Text, Any]]: + + conversation_id = tracker.sender_id + + dispatcher.utter_message( + f"The ID of this conversation is: " f"{conversation_id}." + ) + + dispatcher.utter_message( + f"Trigger an intent with " + f'curl -H "Content-Type: application/json" ' + f'-X POST -d \'{{"name": "EXTERNAL_dry_plant", ' + f'"entities": {{"plant": "Orchid"}}}}\' ' + f"http://localhost:5005/conversations/{conversation_id}/" + f"trigger_intent" + ) + + return [] + + +class ActionWarnDry(Action): + """Informs the user that a plant needs water.""" + + def name(self) -> Text: + return "action_warn_dry" + + async def run( + self, + dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any], + ) -> List[Dict[Text, Any]]: + + plant = next(tracker.get_latest_entity_values("plant"), "someone") + dispatcher.utter_message(f"Your {plant} needs some water!") + + return [] + + +class ForgetReminders(Action): + """Cancels all reminders.""" + + def name(self) -> Text: + return "action_forget_reminders" + + async def run( + self, dispatcher, tracker: Tracker, domain: Dict[Text, Any] + ) -> List[Dict[Text, Any]]: + + dispatcher.utter_message(f"Okay, I'll cancel all your reminders.") + + # Cancel all reminders + return [ReminderCancelled()] diff --git a/examples/reminderbot/config.yml b/examples/reminderbot/config.yml new file mode 100644 index 000000000000..963003b0df8e --- /dev/null +++ b/examples/reminderbot/config.yml @@ -0,0 +1,5 @@ +language: en +pipeline: supervised_embeddings +policies: +- name: MemoizationPolicy +- name: MappingPolicy diff --git a/examples/reminderbot/credentials.yml b/examples/reminderbot/credentials.yml new file mode 100644 index 000000000000..c61e3acddbac --- /dev/null +++ b/examples/reminderbot/credentials.yml @@ -0,0 +1,36 @@ +# This file contains the credentials for the voice & chat platforms +# which your bot is using. +# https://rasa.com/docs/rasa/user-guide/messaging-and-voice-channels/ + +rest: +# you don't need to provide anything here - this channel doesn't +# require any credentials + +callback: + # URL to which Rasa Open Source will send the bot responses + # See https://rasa.com/docs/rasa/user-guide/connectors/your-own-website/#callbackinput + url: "http://localhost:5034/bot" + +#facebook: +# verify: "" +# secret: "" +# page-access-token: "" + +#slack: +# slack_token: "" +# slack_channel: "" + +#socketio: +# user_message_evt: +# bot_message_evt: +# session_persistence: + +#mattermost: +# url: "https:///api/v4" +# team: "" +# user: "" +# pw: "" +# webhook_url: "" + +rasa: + url: "http://localhost:5002/api" diff --git a/examples/reminderbot/data/nlu.md b/examples/reminderbot/data/nlu.md new file mode 100644 index 000000000000..f4afb6ef63f2 --- /dev/null +++ b/examples/reminderbot/data/nlu.md @@ -0,0 +1,43 @@ +## intent:greet +- hey +- hello +- hi +- good morning +- good evening +- hey there + +## intent:bye +- bye +- good bye +- ciao +- see you +- see ya + +## intent:ask_remind_call +- remind me to call [John](name) +- remind me to call [Lis](name) +- remind me to call [Albert](name) +- remind me to call [Susan](name) +- later I have to call [Alan](name) +- later I have to call [Jessie](name) +- later I have to call [Alex](name) +- Please, remind me to call [vova](name) +- please remind me to call [tanja](name) +- I must not forget to call [santa](name) +- I must not forget to call [Daksh](name) +- I must not forget to call [Juste](name) + +## intent:ask_id +- what's the conversation id? +- id +- What is the ID of this conversation? +- How do I send a POST request to this conversation? + +## intent:ask_forget_reminders +- forget about it +- don't remind me! +- Forget about the reminder +- do not remind me +- do not remind me! +- Forget reminding me +- Forget reminding me! diff --git a/examples/reminderbot/data/stories.md b/examples/reminderbot/data/stories.md new file mode 100644 index 000000000000..77fc9ddfb305 --- /dev/null +++ b/examples/reminderbot/data/stories.md @@ -0,0 +1,5 @@ +## happy path +* greet + - utter_what_can_do +* bye + - utter_goodbye \ No newline at end of file diff --git a/examples/reminderbot/domain.yml b/examples/reminderbot/domain.yml new file mode 100644 index 000000000000..0ee3aed3d55d --- /dev/null +++ b/examples/reminderbot/domain.yml @@ -0,0 +1,35 @@ +session_config: + session_expiration_time: 0.0 + carry_over_slots_to_new_session: true +intents: +- greet: + triggers: action_set_reminder +- ask_remind_call: + triggers: action_set_reminder +- ask_forget_reminders: + triggers: action_forget_reminders +- bye: + triggers: utter_goodbye +- ask_id: + triggers: action_tell_id +- EXTERNAL_dry_plant: + triggers: action_warn_dry +- EXTERNAL_reminder: + triggers: action_react_to_reminder +- EXT_reminder +entities: +- name +- plant +responses: + utter_what_can_do: + - text: What can I do for you? + utter_goodbye: + - text: Bye +actions: +- action_set_reminder +- action_forget_reminders +- action_react_to_reminder +- action_tell_id +- action_warn_dry +- utter_what_can_do +- utter_goodbye diff --git a/examples/reminderbot/endpoints.yml b/examples/reminderbot/endpoints.yml new file mode 100644 index 000000000000..2ff5a0923d65 --- /dev/null +++ b/examples/reminderbot/endpoints.yml @@ -0,0 +1,42 @@ +# This file contains the different endpoints your bot can use. + +# Server where the models are pulled from. +# https://rasa.com/docs/rasa/user-guide/running-the-server/#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/core/actions/#custom-actions/ + +action_endpoint: + url: "http://localhost:5055/webhook" + +# Tracker store which is used to store the conversations. +# By default the conversations are stored in memory. +# https://rasa.com/docs/rasa/api/tracker-stores/ + +#tracker_store: +# type: redis +# url: +# port: +# db: +# password: +# use_ssl: + +#tracker_store: +# type: mongod +# url: +# db: +# username: +# password: + +# Event broker which all conversation events should be streamed to. +# https://rasa.com/docs/rasa/api/event-brokers/ + +#event_broker: +# url: localhost +# username: username +# password: password +# queue: queue