diff --git a/changelog/12901.improvement.md b/changelog/12901.improvement.md new file mode 100644 index 000000000000..663ab7ff7af4 --- /dev/null +++ b/changelog/12901.improvement.md @@ -0,0 +1 @@ +Added Schema file and schema validation for flows. \ No newline at end of file diff --git a/docs/docs/action-server/knowledge-base-actions.mdx b/docs/docs/action-server/knowledge-base-actions.mdx index 538c79cdfdfc..956c854fe993 100644 --- a/docs/docs/action-server/knowledge-base-actions.mdx +++ b/docs/docs/action-server/knowledge-base-actions.mdx @@ -563,7 +563,7 @@ You can customize your `InMemoryKnowledgeBase` by overwriting the following func You can overwrite it by calling the function `set_ordinal_mention_mapping()`. If you want to learn more about how this mapping is used, check out [Resolve Mentions](./knowledge-base-actions.mdx#resolve-mentions). -See the [example bot](https://github.com/RasaHQ/rasa/blob/main/examples/knowledgebasebot/actions/actions.py) for an +See the [example bot](https://github.com/RasaHQ/rasa/blob/main/examples/nlu_based/knowledgebasebot/actions/actions.py) for an example implementation of an `InMemoryKnowledgeBase` that uses the method `set_representation_function_of_object()` to overwrite the default representation of the object type “hotel.” The implementation of the `InMemoryKnowledgeBase` itself can be found in the diff --git a/docs/docs/domain.mdx b/docs/docs/domain.mdx index 471768d440fd..512ef26de260 100644 --- a/docs/docs/domain.mdx +++ b/docs/docs/domain.mdx @@ -6,9 +6,9 @@ abstract: The domain defines the universe in which your assistant operates. It s --- Here is a full example of a domain, taken from the -[concertbot](https://github.com/RasaHQ/rasa/tree/main/examples/concertbot) example: +[concertbot](https://github.com/RasaHQ/rasa/tree/main/examples/nlu_based/concertbot) example: -```yaml-rasa (docs/sources/examples/concertbot/domain.yml) +```yaml-rasa (docs/sources/examples/nlu_based/concertbot/domain.yml) ``` ## Multiple Domain Files diff --git a/docs/docs/reaching-out-to-user.mdx b/docs/docs/reaching-out-to-user.mdx index 66dfe1627f13..79e14e7e980b 100644 --- a/docs/docs/reaching-out-to-user.mdx +++ b/docs/docs/reaching-out-to-user.mdx @@ -62,7 +62,7 @@ Sometimes you want an external device to change the course of an ongoing convers For example, if you have a moisture-sensor attached to a Raspberry Pi, you could use it to notify you when a plant needs watering via your assistant. -The examples below are from the [reminderbot example bot](https://github.com/RasaHQ/rasa/blob/main/examples/reminderbot), +The examples below are from the [reminderbot example bot](https://github.com/RasaHQ/rasa/blob/main/examples/nlu_based/reminderbot), which includes both reminders and external events. ### 1. Trigger an Intent @@ -90,7 +90,7 @@ In a real-life scenario, your external device would get the conversation ID from In the dry plant example, you might have a database of plants, the users that water them, and the users' conversation IDs. Your Raspberry Pi would get the conversation ID directly from the database. To try out the reminderbot example locally, you'll need to get the conversation ID manually. See -the reminderbot [README](https://github.com/RasaHQ/rasa/blob/main/examples/reminderbot) for more information. +the reminderbot [README](https://github.com/RasaHQ/rasa/blob/main/examples/nlu_based/reminderbot) for more information. ### 3. Add NLU Training Data @@ -159,7 +159,7 @@ External Events and Reminders don't work in request-response channels like the ` Custom connectors for assistants implementing reminders or external events should be built off of the [CallbackInput channel](./connectors/your-own-website.mdx#callbackinput) instead of the RestInput channel. -See the [reminderbot README](https://github.com/RasaHQ/rasa/blob/main/examples/reminderbot/README.md) +See the [reminderbot README](https://github.com/RasaHQ/rasa/blob/main/examples/nlu_based/reminderbot/README.md) for instructions on how to test your reminders locally. ::: @@ -180,7 +180,7 @@ You should see the bot respond in your channel: ## Reminders You can have your assistant reach out to the user after a set amount of time by using [Reminders](./action-server/events.mdx#reminder). -The examples below are from the [reminderbot example bot](https://github.com/RasaHQ/rasa/blob/main/examples/reminderbot). +The examples below are from the [reminderbot example bot](https://github.com/RasaHQ/rasa/blob/main/examples/nlu_based/reminderbot). You can clone it and follow the instructions in `README` to try out the full version. ### Scheduling Reminders @@ -432,7 +432,7 @@ intents: To try out reminders you'll need to start a [CallbackChannel](./connectors/your-own-website.mdx#callbackinput). You'll also need to start the action server to schedule, react to, and cancel your reminders. -See the [reminderbot README](https://github.com/RasaHQ/rasa/blob/main/examples/reminderbot) for details. +See the [reminderbot README](https://github.com/RasaHQ/rasa/blob/main/examples/nlu_based/reminderbot) for details. Then, if you send the bot a message like `Remind me to call Paul Pots`, you should get a reminder back five minutes later that says `Remember to call Paul Pots!`. diff --git a/docs/package.json b/docs/package.json index 2361812f890d..8c1d8c01bf4c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -115,7 +115,9 @@ "\\.prototyping\\.rasa\\.com", "^https://github\\.com/mit-nlp/MITIE/releases/download/v0\\.4/MITIE-models-v0\\.2\\.tar\\.bz2$", "^https://forum.rasa.com/t/rasa-open-source-2-0-is-out-now-internal-draft/35577$", - "https://docs-test-001.openai.azure.com" + "https://docs-test-001.openai.azure.com", + "^https://github.com/RasaHQ/rasa/tree/main/examples/*", + "^https://github.com/RasaHQ/rasa/blob/main/examples/*" ] } ] diff --git a/examples/money_transfer/actions.py b/examples/money_transfer/actions.py new file mode 100644 index 000000000000..508083283686 --- /dev/null +++ b/examples/money_transfer/actions.py @@ -0,0 +1,19 @@ +from typing import Any, Text, Dict, List +from rasa_sdk import Action, Tracker +from rasa_sdk.executor import CollectingDispatcher +from rasa_sdk.events import SlotSet + +class ActionCheckSufficientFunds(Action): + def name(self) -> Text: + return "action_check_sufficient_funds" + + def run(self, dispatcher: CollectingDispatcher, + tracker: Tracker, + domain: Dict[Text, Any]) -> List[Dict[Text, Any]]: + balance = 1000 # hard-coded for tutorial purposes + # a real api call would look something like + # result = requests.get("https://example.com/api/balance") + # balance = result.json()["balance"] + transfer_amount = tracker.get_slot("amount") + has_sufficient_funds = transfer_amount <= balance + return [SlotSet("has_sufficient_funds", has_sufficient_funds)] diff --git a/examples/money_transfer/config.yml b/examples/money_transfer/config.yml new file mode 100644 index 000000000000..3801f4a2c879 --- /dev/null +++ b/examples/money_transfer/config.yml @@ -0,0 +1,13 @@ +recipe: default.v1 +language: en +pipeline: + - name: LLMCommandGenerator + llm: + model_name: gpt-4 + +policies: + - name: rasa.core.policies.flow_policy.FlowPolicy +# - name: rasa_plus.ml.DocsearchPolicy +# - name: RulePolicy + +assistant_id: 20230405-114328-tranquil-mustard diff --git a/examples/money_transfer/data/flows.yml b/examples/money_transfer/data/flows.yml new file mode 100644 index 000000000000..76ecae6e6e5d --- /dev/null +++ b/examples/money_transfer/data/flows.yml @@ -0,0 +1,23 @@ +flows: + transfer_money: + description: This flow lets users send money to friends and family. + steps: + - collect: recipient + - collect: amount + - action: action_check_sufficient_funds + next: + - if: not has_sufficient_funds + then: + - action: utter_insufficient_funds + next: END + - else: final_confirmation + - id: final_confirmation + collect: final_confirmation + next: + - if: not final_confirmation + then: + - action: utter_transfer_cancelled + next: END + - else: transfer_successful + - id: transfer_successful + action: utter_transfer_complete diff --git a/examples/money_transfer/domain.yml b/examples/money_transfer/domain.yml new file mode 100644 index 000000000000..6d2cd6f1304c --- /dev/null +++ b/examples/money_transfer/domain.yml @@ -0,0 +1,43 @@ +version: "3.1" + +slots: + recipient: + type: text + mappings: + - type: custom + amount: + type: float + mappings: + - type: custom + final_confirmation: + type: bool + mappings: + - type: custom + has_sufficient_funds: + type: bool + mappings: + - type: custom + +responses: + utter_ask_recipient: + - text: "Who would you like to send money to?" + + utter_ask_amount: + - text: "How much money would you like to send?" + + utter_transfer_complete: + - text: "All done. ${amount} has been sent to {recipient}." + + utter_ask_final_confirmation: + - text: "Please confirm: you want to transfer ${amount} to {recipient}?" + + utter_transfer_cancelled: + - text: "Your transfer has been cancelled." + + utter_insufficient_funds: + - text: "You do not have enough funds to make this transaction." + + + +actions: + - action_check_sufficient_funds diff --git a/examples/money_transfer/e2e_tests/complete_request.yml b/examples/money_transfer/e2e_tests/complete_request.yml new file mode 100644 index 000000000000..99072cb957c9 --- /dev/null +++ b/examples/money_transfer/e2e_tests/complete_request.yml @@ -0,0 +1,7 @@ +test_cases: + - test_case: user corrects recipient in the next message + steps: + - user: I want to send 400 dollars to James + - utter: utter_ask_final_confirmation + - user: yes + - utter: utter_transfer_complete diff --git a/examples/reminderbot/endpoints.yml b/examples/money_transfer/endpoints.yml similarity index 100% rename from examples/reminderbot/endpoints.yml rename to examples/money_transfer/endpoints.yml diff --git a/examples/concertbot/actions/__init__.py b/examples/nlu_based/__init__.py similarity index 100% rename from examples/concertbot/actions/__init__.py rename to examples/nlu_based/__init__.py diff --git a/examples/concertbot/README.md b/examples/nlu_based/concertbot/README.md similarity index 100% rename from examples/concertbot/README.md rename to examples/nlu_based/concertbot/README.md diff --git a/examples/formbot/actions/__init__.py b/examples/nlu_based/concertbot/actions/__init__.py similarity index 100% rename from examples/formbot/actions/__init__.py rename to examples/nlu_based/concertbot/actions/__init__.py diff --git a/examples/concertbot/actions/actions.py b/examples/nlu_based/concertbot/actions/actions.py similarity index 100% rename from examples/concertbot/actions/actions.py rename to examples/nlu_based/concertbot/actions/actions.py diff --git a/examples/concertbot/config.yml b/examples/nlu_based/concertbot/config.yml similarity index 100% rename from examples/concertbot/config.yml rename to examples/nlu_based/concertbot/config.yml diff --git a/examples/concertbot/data/nlu.yml b/examples/nlu_based/concertbot/data/nlu.yml similarity index 100% rename from examples/concertbot/data/nlu.yml rename to examples/nlu_based/concertbot/data/nlu.yml diff --git a/examples/concertbot/data/rules.yml b/examples/nlu_based/concertbot/data/rules.yml similarity index 100% rename from examples/concertbot/data/rules.yml rename to examples/nlu_based/concertbot/data/rules.yml diff --git a/examples/concertbot/data/stories.yml b/examples/nlu_based/concertbot/data/stories.yml similarity index 100% rename from examples/concertbot/data/stories.yml rename to examples/nlu_based/concertbot/data/stories.yml diff --git a/examples/concertbot/domain.yml b/examples/nlu_based/concertbot/domain.yml similarity index 100% rename from examples/concertbot/domain.yml rename to examples/nlu_based/concertbot/domain.yml diff --git a/examples/concertbot/endpoints.yml b/examples/nlu_based/concertbot/endpoints.yml similarity index 100% rename from examples/concertbot/endpoints.yml rename to examples/nlu_based/concertbot/endpoints.yml diff --git a/examples/e2ebot/config.yml b/examples/nlu_based/e2ebot/config.yml similarity index 100% rename from examples/e2ebot/config.yml rename to examples/nlu_based/e2ebot/config.yml diff --git a/examples/e2ebot/data/nlu.yml b/examples/nlu_based/e2ebot/data/nlu.yml similarity index 100% rename from examples/e2ebot/data/nlu.yml rename to examples/nlu_based/e2ebot/data/nlu.yml diff --git a/examples/e2ebot/data/stories.yml b/examples/nlu_based/e2ebot/data/stories.yml similarity index 100% rename from examples/e2ebot/data/stories.yml rename to examples/nlu_based/e2ebot/data/stories.yml diff --git a/examples/e2ebot/domain.yml b/examples/nlu_based/e2ebot/domain.yml similarity index 100% rename from examples/e2ebot/domain.yml rename to examples/nlu_based/e2ebot/domain.yml diff --git a/examples/e2ebot/tests/test_stories.yml b/examples/nlu_based/e2ebot/tests/test_stories.yml similarity index 100% rename from examples/e2ebot/tests/test_stories.yml rename to examples/nlu_based/e2ebot/tests/test_stories.yml diff --git a/examples/formbot/README.md b/examples/nlu_based/formbot/README.md similarity index 100% rename from examples/formbot/README.md rename to examples/nlu_based/formbot/README.md diff --git a/examples/knowledgebasebot/actions/__init__.py b/examples/nlu_based/formbot/actions/__init__.py similarity index 100% rename from examples/knowledgebasebot/actions/__init__.py rename to examples/nlu_based/formbot/actions/__init__.py diff --git a/examples/formbot/actions/actions.py b/examples/nlu_based/formbot/actions/actions.py similarity index 100% rename from examples/formbot/actions/actions.py rename to examples/nlu_based/formbot/actions/actions.py diff --git a/examples/formbot/config.yml b/examples/nlu_based/formbot/config.yml similarity index 100% rename from examples/formbot/config.yml rename to examples/nlu_based/formbot/config.yml diff --git a/examples/formbot/data/nlu.yml b/examples/nlu_based/formbot/data/nlu.yml similarity index 100% rename from examples/formbot/data/nlu.yml rename to examples/nlu_based/formbot/data/nlu.yml diff --git a/examples/formbot/data/rules.yml b/examples/nlu_based/formbot/data/rules.yml similarity index 100% rename from examples/formbot/data/rules.yml rename to examples/nlu_based/formbot/data/rules.yml diff --git a/examples/formbot/data/stories.yml b/examples/nlu_based/formbot/data/stories.yml similarity index 100% rename from examples/formbot/data/stories.yml rename to examples/nlu_based/formbot/data/stories.yml diff --git a/examples/formbot/domain.yml b/examples/nlu_based/formbot/domain.yml similarity index 100% rename from examples/formbot/domain.yml rename to examples/nlu_based/formbot/domain.yml diff --git a/examples/formbot/endpoints.yml b/examples/nlu_based/formbot/endpoints.yml similarity index 100% rename from examples/formbot/endpoints.yml rename to examples/nlu_based/formbot/endpoints.yml diff --git a/examples/formbot/tests/test_stories.yml b/examples/nlu_based/formbot/tests/test_stories.yml similarity index 100% rename from examples/formbot/tests/test_stories.yml rename to examples/nlu_based/formbot/tests/test_stories.yml diff --git a/examples/knowledgebasebot/README.md b/examples/nlu_based/knowledgebasebot/README.md similarity index 100% rename from examples/knowledgebasebot/README.md rename to examples/nlu_based/knowledgebasebot/README.md diff --git a/examples/reminderbot/actions/__init__.py b/examples/nlu_based/knowledgebasebot/actions/__init__.py similarity index 100% rename from examples/reminderbot/actions/__init__.py rename to examples/nlu_based/knowledgebasebot/actions/__init__.py diff --git a/examples/knowledgebasebot/actions/actions.py b/examples/nlu_based/knowledgebasebot/actions/actions.py similarity index 100% rename from examples/knowledgebasebot/actions/actions.py rename to examples/nlu_based/knowledgebasebot/actions/actions.py diff --git a/examples/knowledgebasebot/config.yml b/examples/nlu_based/knowledgebasebot/config.yml similarity index 100% rename from examples/knowledgebasebot/config.yml rename to examples/nlu_based/knowledgebasebot/config.yml diff --git a/examples/knowledgebasebot/data/nlu.yml b/examples/nlu_based/knowledgebasebot/data/nlu.yml similarity index 100% rename from examples/knowledgebasebot/data/nlu.yml rename to examples/nlu_based/knowledgebasebot/data/nlu.yml diff --git a/examples/knowledgebasebot/data/rules.yml b/examples/nlu_based/knowledgebasebot/data/rules.yml similarity index 100% rename from examples/knowledgebasebot/data/rules.yml rename to examples/nlu_based/knowledgebasebot/data/rules.yml diff --git a/examples/knowledgebasebot/data/stories.yml b/examples/nlu_based/knowledgebasebot/data/stories.yml similarity index 100% rename from examples/knowledgebasebot/data/stories.yml rename to examples/nlu_based/knowledgebasebot/data/stories.yml diff --git a/examples/knowledgebasebot/domain.yml b/examples/nlu_based/knowledgebasebot/domain.yml similarity index 100% rename from examples/knowledgebasebot/domain.yml rename to examples/nlu_based/knowledgebasebot/domain.yml diff --git a/examples/knowledgebasebot/endpoints.yml b/examples/nlu_based/knowledgebasebot/endpoints.yml similarity index 100% rename from examples/knowledgebasebot/endpoints.yml rename to examples/nlu_based/knowledgebasebot/endpoints.yml diff --git a/examples/knowledgebasebot/knowledge_base_data.json b/examples/nlu_based/knowledgebasebot/knowledge_base_data.json similarity index 100% rename from examples/knowledgebasebot/knowledge_base_data.json rename to examples/nlu_based/knowledgebasebot/knowledge_base_data.json diff --git a/examples/moodbot/README.md b/examples/nlu_based/moodbot/README.md similarity index 100% rename from examples/moodbot/README.md rename to examples/nlu_based/moodbot/README.md diff --git a/examples/moodbot/config.yml b/examples/nlu_based/moodbot/config.yml similarity index 100% rename from examples/moodbot/config.yml rename to examples/nlu_based/moodbot/config.yml diff --git a/examples/moodbot/credentials.yml b/examples/nlu_based/moodbot/credentials.yml similarity index 100% rename from examples/moodbot/credentials.yml rename to examples/nlu_based/moodbot/credentials.yml diff --git a/examples/moodbot/data/nlu.yml b/examples/nlu_based/moodbot/data/nlu.yml similarity index 100% rename from examples/moodbot/data/nlu.yml rename to examples/nlu_based/moodbot/data/nlu.yml diff --git a/examples/moodbot/data/rules.yml b/examples/nlu_based/moodbot/data/rules.yml similarity index 100% rename from examples/moodbot/data/rules.yml rename to examples/nlu_based/moodbot/data/rules.yml diff --git a/examples/moodbot/data/stories.yml b/examples/nlu_based/moodbot/data/stories.yml similarity index 100% rename from examples/moodbot/data/stories.yml rename to examples/nlu_based/moodbot/data/stories.yml diff --git a/examples/moodbot/domain.yml b/examples/nlu_based/moodbot/domain.yml similarity index 100% rename from examples/moodbot/domain.yml rename to examples/nlu_based/moodbot/domain.yml diff --git a/examples/reminderbot/README.md b/examples/nlu_based/reminderbot/README.md similarity index 100% rename from examples/reminderbot/README.md rename to examples/nlu_based/reminderbot/README.md diff --git a/examples/rules/actions/__init__.py b/examples/nlu_based/reminderbot/actions/__init__.py similarity index 100% rename from examples/rules/actions/__init__.py rename to examples/nlu_based/reminderbot/actions/__init__.py diff --git a/examples/reminderbot/actions/actions.py b/examples/nlu_based/reminderbot/actions/actions.py similarity index 100% rename from examples/reminderbot/actions/actions.py rename to examples/nlu_based/reminderbot/actions/actions.py diff --git a/examples/reminderbot/callback_server.py b/examples/nlu_based/reminderbot/callback_server.py similarity index 100% rename from examples/reminderbot/callback_server.py rename to examples/nlu_based/reminderbot/callback_server.py diff --git a/examples/reminderbot/config.yml b/examples/nlu_based/reminderbot/config.yml similarity index 100% rename from examples/reminderbot/config.yml rename to examples/nlu_based/reminderbot/config.yml diff --git a/examples/reminderbot/credentials.yml b/examples/nlu_based/reminderbot/credentials.yml similarity index 100% rename from examples/reminderbot/credentials.yml rename to examples/nlu_based/reminderbot/credentials.yml diff --git a/examples/reminderbot/data/nlu.yml b/examples/nlu_based/reminderbot/data/nlu.yml similarity index 100% rename from examples/reminderbot/data/nlu.yml rename to examples/nlu_based/reminderbot/data/nlu.yml diff --git a/examples/reminderbot/data/rules.yml b/examples/nlu_based/reminderbot/data/rules.yml similarity index 100% rename from examples/reminderbot/data/rules.yml rename to examples/nlu_based/reminderbot/data/rules.yml diff --git a/examples/reminderbot/data/stories.yml b/examples/nlu_based/reminderbot/data/stories.yml similarity index 100% rename from examples/reminderbot/data/stories.yml rename to examples/nlu_based/reminderbot/data/stories.yml diff --git a/examples/reminderbot/domain.yml b/examples/nlu_based/reminderbot/domain.yml similarity index 100% rename from examples/reminderbot/domain.yml rename to examples/nlu_based/reminderbot/domain.yml diff --git a/examples/nlu_based/reminderbot/endpoints.yml b/examples/nlu_based/reminderbot/endpoints.yml new file mode 100644 index 000000000000..5f65275b8802 --- /dev/null +++ b/examples/nlu_based/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/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" + +# Tracker store which is used to store the conversations. +# By default the conversations are stored in memory. +# https://rasa.com/docs/rasa/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/event-brokers + +#event_broker: +# url: localhost +# username: username +# password: password +# queue: queue diff --git a/examples/responseselectorbot/README.md b/examples/nlu_based/responseselectorbot/README.md similarity index 100% rename from examples/responseselectorbot/README.md rename to examples/nlu_based/responseselectorbot/README.md diff --git a/examples/responseselectorbot/config.yml b/examples/nlu_based/responseselectorbot/config.yml similarity index 100% rename from examples/responseselectorbot/config.yml rename to examples/nlu_based/responseselectorbot/config.yml diff --git a/examples/responseselectorbot/data/nlu.yml b/examples/nlu_based/responseselectorbot/data/nlu.yml similarity index 100% rename from examples/responseselectorbot/data/nlu.yml rename to examples/nlu_based/responseselectorbot/data/nlu.yml diff --git a/examples/responseselectorbot/data/rules.yml b/examples/nlu_based/responseselectorbot/data/rules.yml similarity index 100% rename from examples/responseselectorbot/data/rules.yml rename to examples/nlu_based/responseselectorbot/data/rules.yml diff --git a/examples/responseselectorbot/data/stories.yml b/examples/nlu_based/responseselectorbot/data/stories.yml similarity index 100% rename from examples/responseselectorbot/data/stories.yml rename to examples/nlu_based/responseselectorbot/data/stories.yml diff --git a/examples/responseselectorbot/domain.yml b/examples/nlu_based/responseselectorbot/domain.yml similarity index 100% rename from examples/responseselectorbot/domain.yml rename to examples/nlu_based/responseselectorbot/domain.yml diff --git a/examples/nlu_based/rules/actions/__init__.py b/examples/nlu_based/rules/actions/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/examples/rules/actions/actions.py b/examples/nlu_based/rules/actions/actions.py similarity index 100% rename from examples/rules/actions/actions.py rename to examples/nlu_based/rules/actions/actions.py diff --git a/examples/rules/config.yml b/examples/nlu_based/rules/config.yml similarity index 100% rename from examples/rules/config.yml rename to examples/nlu_based/rules/config.yml diff --git a/examples/rules/data/nlu.yml b/examples/nlu_based/rules/data/nlu.yml similarity index 100% rename from examples/rules/data/nlu.yml rename to examples/nlu_based/rules/data/nlu.yml diff --git a/examples/rules/data/rules.yml b/examples/nlu_based/rules/data/rules.yml similarity index 100% rename from examples/rules/data/rules.yml rename to examples/nlu_based/rules/data/rules.yml diff --git a/examples/rules/domain.yml b/examples/nlu_based/rules/domain.yml similarity index 100% rename from examples/rules/domain.yml rename to examples/nlu_based/rules/domain.yml diff --git a/examples/rules/endpoints.yml b/examples/nlu_based/rules/endpoints.yml similarity index 100% rename from examples/rules/endpoints.yml rename to examples/nlu_based/rules/endpoints.yml diff --git a/pyproject.toml b/pyproject.toml index fe2feb58044c..18211d12110f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ exclude = "((.eggs | .git | .pytest_cache | build | dist))" [tool.poetry] name = "rasa" -version = "3.8.0a11" +version = "3.8.0a12" description = "Open source machine learning framework to automate text- and voice-based conversations: NLU, dialogue management, connect to Slack, Facebook, and more - Create chatbots and voice assistants" authors = [ "Rasa Technologies GmbH ",] maintainers = [ "Tom Bocklisch ",] diff --git a/rasa/cli/project_templates/tutorial/data/flows.yml b/rasa/cli/project_templates/tutorial/data/flows.yml index 040a96b776f1..d849432a00e6 100644 --- a/rasa/cli/project_templates/tutorial/data/flows.yml +++ b/rasa/cli/project_templates/tutorial/data/flows.yml @@ -2,11 +2,6 @@ flows: transfer_money: description: This flow lets users send money to friends and family. steps: - - id: "ask_recipient" - collect: recipient - next: "ask_amount" - - id: "ask_amount" - collect: amount - next: "transfer_successful" - - id: "transfer_successful" - action: utter_transfer_complete + - collect: recipient + - collect: amount + - action: utter_transfer_complete diff --git a/rasa/core/actions/action.py b/rasa/core/actions/action.py index 6a88d8e62927..232b977d2db8 100644 --- a/rasa/core/actions/action.py +++ b/rasa/core/actions/action.py @@ -37,6 +37,7 @@ ) from rasa.shared.core import events from rasa.shared.core.constants import ( + DIALOGUE_STACK_SLOT, USER_INTENT_OUT_OF_SCOPE, ACTION_LISTEN_NAME, ACTION_RESTART_NAME, @@ -829,6 +830,13 @@ async def run( ) evts = events.deserialise_events(events_json) + # filter out `SlotSet` events for internal `dialogue_stack` slot + evts = [ + event + for event in evts + if not (isinstance(event, SlotSet) and event.key == DIALOGUE_STACK_SLOT) + ] + return cast(List[Event], bot_messages) + evts except ClientResponseError as e: diff --git a/rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml b/rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml index cf978a6a0071..983f4154b7b6 100644 --- a/rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml +++ b/rasa/dialogue_understanding/patterns/default_flows_for_patterns.yml @@ -125,7 +125,7 @@ flows: - action: validate_{{context.collect}} next: - if: "{{context.collect}} is not null" - then: "done" + then: "END" - else: "ask_collect" - id: "ask_collect" action: "{{context.utter}}" @@ -133,7 +133,7 @@ flows: - id: "listen" action: action_listen next: "start" - - id: "done" + pattern_code_change: description: flow used to clean the stack after a bot update diff --git a/rasa/shared/core/domain.py b/rasa/shared/core/domain.py index 62132cd2d5bf..73531d08271d 100644 --- a/rasa/shared/core/domain.py +++ b/rasa/shared/core/domain.py @@ -993,7 +993,12 @@ def _add_flow_slots(self) -> None: for flow_slot in FLOW_SLOT_NAMES: if flow_slot not in slot_names: self.slots.append( - AnySlot(flow_slot, mappings=[], influence_conversation=False) + AnySlot( + flow_slot, + mappings=[], + influence_conversation=False, + is_builtin=True, + ) ) else: # TODO: figure out what to do here. @@ -1016,6 +1021,7 @@ def _add_requested_slot(self) -> None: rasa.shared.core.constants.REQUESTED_SLOT, mappings=[], influence_conversation=False, + is_builtin=True, ) ) @@ -1041,12 +1047,21 @@ def _add_knowledge_base_slots(self) -> None: for slot in KNOWLEDGE_BASE_SLOT_NAMES: if slot not in slot_names: self.slots.append( - TextSlot(slot, mappings=[], influence_conversation=False) + TextSlot( + slot, + mappings=[], + influence_conversation=False, + is_builtin=True, + ) ) def _add_session_metadata_slot(self) -> None: self.slots.append( - AnySlot(rasa.shared.core.constants.SESSION_START_METADATA_SLOT, mappings=[]) + AnySlot( + rasa.shared.core.constants.SESSION_START_METADATA_SLOT, + mappings=[], + is_builtin=True, + ) ) def index_for_action(self, action_name: Text) -> int: diff --git a/rasa/shared/core/flows/flows_yaml_schema.json b/rasa/shared/core/flows/flows_yaml_schema.json new file mode 100644 index 000000000000..dd5b08b2a89f --- /dev/null +++ b/rasa/shared/core/flows/flows_yaml_schema.json @@ -0,0 +1,286 @@ +{ + "type": "object", + "required": [ + "flows" + ], + "properties": { + "version": { + "type": "string" + }, + "flows": { + "type": "object", + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { + "$ref": "#$defs/flow" + } + } + } + }, + "$defs": { + "steps": { + "type": "array", + "minContains": 1, + "items": { + "type": "object", + "oneOf": [ + { + "required": [ + "action" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "action": { + "type": "string" + }, + "next": { + "$ref": "#$defs/next" + } + } + }, + { + "required": [ + "collect" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "description":{ + "type": "string" + }, + "collect": { + "type": "string" + }, + "ask_before_filling": { + "type": "boolean" + }, + "reset_after_flow_ends": { + "type": "boolean" + }, + "utter": { + "type": "string" + }, + "rejections": { + "type": "array", + "items": { + "type": "object", + "required": [ + "if", + "utter" + ], + "properties": { + "if": { + "type": "string" + }, + "utter": { + "type": "string" + } + } + } + }, + "next": { + "$ref": "#$defs/next" + } + } + }, + { + "required": [ + "link" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "link": { + "type": "string" + }, + "next": { + "type": "null" + } + } + }, + { + "required": [ + "set_slots" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "set_slots": { + "$ref": "#$defs/set_slots" + }, + "next": { + "$ref": "#$defs/next" + } + } + }, + { + "required": [ + "next" + ], + "additionalProperties": false, + "properties": { + "next": { + "$ref": "#$defs/next" + }, + "id": { + "type": "string" + } + } + }, + { + "required": [ + "generation_prompt" + ], + "additionalProperties": false, + "properties": { + "generation_prompt": { + "type": "string" + }, + "id": { + "type": "string" + }, + "next": { + "$ref": "#$defs/next" + } + } + }, + { + "required": [ + "entry_prompt" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "entry_prompt": { + "type": "string" + }, + "next": { + "$ref": "#$defs/next" + } + } + } + ] + } + }, + "flow": { + "required": [ + "steps" + ], + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "if": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nlu_trigger": { + "type": "array", + "items": { + "required": [ + "intent" + ], + "type": "object", + "additionalProperties": false, + "properties": { + "intent": { + "type": "object", + "properties": { + "confidence_threshold": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "steps": { + "$ref": "#$defs/steps" + } + } + }, + "next": { + "anyOf": [ + { + "type": "array", + "minContains": 1, + "items": { + "type": "object", + "oneOf": [ + { + "required": [ + "if", + "then" + ] + }, + { + "required": [ + "else" + ] + } + ], + "properties": { + "else": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#$defs/steps" + } + ] + }, + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "$ref": "#$defs/steps" + }, + { + "type": "string" + } + ] + } + } + } + }, + { + "type": "string" + } + ] + }, + "set_slots": { + "type": "array", + "items": { + "type": "object", + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": { + "type": ["string", "null", "boolean", "number"] + } + } + } + } + } +} diff --git a/rasa/shared/core/flows/flows_yaml_schema.yml b/rasa/shared/core/flows/flows_yaml_schema.yml deleted file mode 100644 index af6f24a468c3..000000000000 --- a/rasa/shared/core/flows/flows_yaml_schema.yml +++ /dev/null @@ -1,8 +0,0 @@ -allowempty: True -mapping: - version: - type: "str" - required: False - allowempty: False - flows: - type: "any" diff --git a/rasa/shared/core/flows/yaml_flows_io.py b/rasa/shared/core/flows/yaml_flows_io.py index f4e430bbb802..14cf7da82557 100644 --- a/rasa/shared/core/flows/yaml_flows_io.py +++ b/rasa/shared/core/flows/yaml_flows_io.py @@ -1,6 +1,7 @@ import textwrap from pathlib import Path from typing import List, Text, Union + from rasa.shared.core.flows.utils import KEY_FLOWS import rasa.shared.utils.io @@ -9,7 +10,7 @@ from rasa.shared.core.flows.flow import Flow, FlowsList -FLOWS_SCHEMA_FILE = "/shared/core/flows/flows_yaml_schema.yml" +FLOWS_SCHEMA_FILE = "shared/core/flows/flows_yaml_schema.json" class YAMLFlowsReader: @@ -53,7 +54,9 @@ def read_from_string(cls, string: Text, skip_validation: bool = False) -> FlowsL `Flow`s read from `string`. """ if not skip_validation: - rasa.shared.utils.validation.validate_yaml_schema(string, FLOWS_SCHEMA_FILE) + rasa.shared.utils.validation.validate_yaml_with_jsonschema( + string, FLOWS_SCHEMA_FILE + ) yaml_content = rasa.shared.utils.io.read_yaml(string) diff --git a/rasa/shared/core/slots.py b/rasa/shared/core/slots.py index b6f6eb44415b..9630d00d7f76 100644 --- a/rasa/shared/core/slots.py +++ b/rasa/shared/core/slots.py @@ -35,6 +35,7 @@ def __init__( initial_value: Any = None, value_reset_delay: Optional[int] = None, influence_conversation: bool = True, + is_builtin: bool = False, ) -> None: """Create a Slot. @@ -46,6 +47,9 @@ def __init__( initial_value. This is behavior is currently not implemented. influence_conversation: If `True` the slot will be featurized and hence influence the predictions of the dialogue polices. + is_builtin: `True` if the slot is a built-in slot, `False` otherwise. + Built-in slots also encompass user writable slots (via custom actions), + such as `return_value`. """ self.name = name self.mappings = mappings @@ -54,6 +58,7 @@ def __init__( self._value_reset_delay = value_reset_delay self.influence_conversation = influence_conversation self._has_been_set = False + self.is_builtin = is_builtin def feature_dimensionality(self) -> int: """How many features this single slot creates. @@ -180,6 +185,7 @@ def __init__( max_value: float = 1.0, min_value: float = 0.0, influence_conversation: bool = True, + is_builtin: bool = False, ) -> None: """Creates a FloatSlot. @@ -188,7 +194,12 @@ def __init__( UserWarning, if initial_value is outside the min-max range. """ super().__init__( - name, mappings, initial_value, value_reset_delay, influence_conversation + name, + mappings, + initial_value, + value_reset_delay, + influence_conversation, + is_builtin, ) self.max_value = max_value self.min_value = min_value @@ -318,10 +329,16 @@ def __init__( initial_value: Any = None, value_reset_delay: Optional[int] = None, influence_conversation: bool = True, + is_builtin: bool = False, ) -> None: """Creates a `Categorical Slot` (see parent class for detailed docstring).""" super().__init__( - name, mappings, initial_value, value_reset_delay, influence_conversation + name, + mappings, + initial_value, + value_reset_delay, + influence_conversation, + is_builtin, ) if values and None in values: rasa.shared.utils.io.raise_warning( @@ -417,6 +434,7 @@ def __init__( initial_value: Any = None, value_reset_delay: Optional[int] = None, influence_conversation: bool = False, + is_builtin: bool = False, ) -> None: """Creates an `Any Slot` (see parent class for detailed docstring). @@ -433,7 +451,12 @@ def __init__( ) super().__init__( - name, mappings, initial_value, value_reset_delay, influence_conversation + name, + mappings, + initial_value, + value_reset_delay, + influence_conversation, + is_builtin, ) def __eq__(self, other: Any) -> bool: diff --git a/rasa/shared/utils/validation.py b/rasa/shared/utils/validation.py index 04a0d5b43da6..a91ff4c7a37a 100644 --- a/rasa/shared/utils/validation.py +++ b/rasa/shared/utils/validation.py @@ -289,3 +289,44 @@ def validate_training_data_format_version( docs=DOCS_URL_TRAINING_DATA, ) return False + + +def validate_yaml_with_jsonschema( + yaml_file_content: Text, schema_path: Text, package_name: Text = PACKAGE_NAME +) -> None: + """Validate data format. + + Args: + yaml_file_content: the content of the yaml file to be validated + schema_path: the schema of the yaml file + package_name: the name of the package the schema is located in. defaults + to `rasa`. + + Raises: + YamlSyntaxException: if the yaml file is not valid. + SchemaValidationError: if validation fails. + """ + from jsonschema import validate, ValidationError + from ruamel.yaml import YAMLError + import pkg_resources + + schema_file = pkg_resources.resource_filename(package_name, schema_path) + schema_content = rasa.shared.utils.io.read_json_file(schema_file) + + try: + # we need "rt" since + # it will add meta information to the parsed output. this meta information + # will include e.g. at which line an object was parsed. this is very + # helpful when we validate files later on and want to point the user to the + # right line + source_data = rasa.shared.utils.io.read_yaml( + yaml_file_content, reader_type=["safe", "rt"] + ) + except (YAMLError, DuplicateKeyError) as e: + raise YamlSyntaxException(underlying_yaml_exception=e) + + try: + validate(source_data, schema_content) + except ValidationError as error: + error.message += ". Failed to validate data, make sure your data is valid." + raise SchemaValidationError.create_from(error) from error diff --git a/rasa/version.py b/rasa/version.py index d15a4a5d8a6b..0a2e252183fd 100644 --- a/rasa/version.py +++ b/rasa/version.py @@ -1,3 +1,3 @@ # this file will automatically be changed, # do not add anything but the version number here! -__version__ = "3.8.0a11" +__version__ = "3.8.0a12" diff --git a/tests/cli/test_rasa_data.py b/tests/cli/test_rasa_data.py index f8c4dc674362..b9644a7ab5c8 100644 --- a/tests/cli/test_rasa_data.py +++ b/tests/cli/test_rasa_data.py @@ -270,10 +270,10 @@ def test_rasa_data_validate_flows_success( name: transfer money steps: - id: "ask_recipient" - collect_information: transfer_recipient + collect: "transfer_recipient" next: "ask_amount" - id: "ask_amount" - collect_information: transfer_amount + collect: "transfer_amount" next: "execute_transfer" - id: "execute_transfer" action: action_transfer_money""" diff --git a/tests/core/conftest.py b/tests/core/conftest.py index ff69c4acec19..1da3c380e10d 100644 --- a/tests/core/conftest.py +++ b/tests/core/conftest.py @@ -135,11 +135,11 @@ def default_tracker(domain: Domain) -> DialogueStateTracker: @pytest.fixture(scope="session") async def trained_formbot(trained_async: Callable) -> Text: return await trained_async( - domain="examples/formbot/domain.yml", - config="examples/formbot/config.yml", + domain="examples/nlu_based/formbot/domain.yml", + config="examples/nlu_based/formbot/config.yml", training_files=[ - "examples/formbot/data/rules.yml", - "examples/formbot/data/stories.yml", + "examples/nlu_based/formbot/data/rules.yml", + "examples/nlu_based/formbot/data/stories.yml", ], ) diff --git a/tests/core/policies/test_flow_policy.py b/tests/core/policies/test_flow_policy.py index a4b66a202ab6..6575a4f679e6 100644 --- a/tests/core/policies/test_flow_policy.py +++ b/tests/core/policies/test_flow_policy.py @@ -93,6 +93,7 @@ def _run_flow_until_listen( return actions, events +@pytest.mark.skip(reason="Skip until intent gets replaced by nlu_trigger") def test_select_next_action() -> None: flows = YAMLFlowsReader.read_from_string( textwrap.dedent( @@ -212,12 +213,12 @@ def test_executor_trips_internal_circuit_breaker(): foo_flow: steps: - id: "1" - set_slot: - foo: bar + set_slots: + - foo: bar next: "2" - id: "2" - set_slot: - foo: barbar + set_slots: + - foo: barbar next: "1" """ ) @@ -250,12 +251,12 @@ def test_policy_triggers_error_pattern_if_internal_circuit_breaker_is_tripped( foo_flow: steps: - id: "1" - set_slot: - foo: bar + set_slots: + - foo: bar next: "2" - id: "2" - set_slot: - foo: barbar + set_slots: + - foo: barbar next: "1" """ ) @@ -301,8 +302,8 @@ def test_executor_does_not_get_tripped_if_an_action_is_predicted_in_loop(): foo_flow: steps: - id: "1" - set_slot: - foo: bar + set_slots: + - foo: bar next: "2" - id: "2" action: action_listen diff --git a/tests/core/policies/test_rule_policy.py b/tests/core/policies/test_rule_policy.py index 945a6717a826..9fa5cdee415c 100644 --- a/tests/core/policies/test_rule_policy.py +++ b/tests/core/policies/test_rule_policy.py @@ -758,7 +758,7 @@ def test_rule_policy_finetune( ) original_data = training.load_data( - "examples/rules/data/rules.yml", trained_rule_policy_domain + "examples/nlu_based/rules/data/rules.yml", trained_rule_policy_domain ) loaded_policy.train(original_data + [new_rule], trained_rule_policy_domain) @@ -805,7 +805,7 @@ def test_rule_policy_contradicting_rule_finetune( ) original_data = training.load_data( - "examples/rules/data/rules.yml", trained_rule_policy_domain + "examples/nlu_based/rules/data/rules.yml", trained_rule_policy_domain ) with pytest.raises(InvalidRule) as execinfo: @@ -1847,7 +1847,7 @@ def test_immediate_submit(policy: RulePolicy): @pytest.fixture() def trained_rule_policy_domain() -> Domain: - return Domain.load("examples/rules/domain.yml") + return Domain.load("examples/nlu_based/rules/domain.yml") @pytest.fixture() @@ -1855,7 +1855,7 @@ def trained_rule_policy( trained_rule_policy_domain: Domain, policy: RulePolicy ) -> RulePolicy: trackers = training.load_data( - "examples/rules/data/rules.yml", trained_rule_policy_domain + "examples/nlu_based/rules/data/rules.yml", trained_rule_policy_domain ) policy.train(trackers, trained_rule_policy_domain) diff --git a/tests/core/test_actions.py b/tests/core/test_actions.py index 2b80edef8876..61a3638dab62 100644 --- a/tests/core/test_actions.py +++ b/tests/core/test_actions.py @@ -3039,3 +3039,25 @@ def test_default_actions_and_names_consistency(): RULE_SNIPPET_ACTION_NAME } assert names_of_default_actions == names_of_executable_actions_in_constants + + +async def test_filter_out_dialogue_stack_slot_set_in_a_custom_action( + default_channel: OutputChannel, + default_nlg: NaturalLanguageGenerator, + default_tracker: DialogueStateTracker, + domain: Domain, +) -> None: + endpoint = EndpointConfig("https://example.com/webhooks/actions") + remote_action = action.RemoteAction("my_action", endpoint) + events = [SlotSet(DIALOGUE_STACK_SLOT, {}), SlotSet("some_slot", "some_value")] + events_as_dict = [event.as_dict() for event in events] + response = {"events": events_as_dict, "responses": []} + with aioresponses() as mocked: + mocked.post("https://example.com/webhooks/actions", payload=response) + + events = await remote_action.run( + default_channel, default_nlg, default_tracker, domain + ) + + assert len(events) == 1 + assert events[0] == SlotSet("some_slot", "some_value") diff --git a/tests/dialogue_understanding/generator/test_llm_command_generator.py b/tests/dialogue_understanding/generator/test_llm_command_generator.py index cbd73e6afde6..56c50fdcf8dd 100644 --- a/tests/dialogue_understanding/generator/test_llm_command_generator.py +++ b/tests/dialogue_understanding/generator/test_llm_command_generator.py @@ -1,7 +1,11 @@ +import uuid + from typing import Optional, Any from unittest.mock import Mock, patch import pytest +from _pytest.tmpdir import TempPathFactory + from structlog.testing import capture_logs from rasa.dialogue_understanding.generator.llm_command_generator import ( @@ -18,6 +22,9 @@ KnowledgeAnswerCommand, ClarifyCommand, ) +from rasa.engine.storage.local_model_storage import LocalModelStorage +from rasa.engine.storage.resource import Resource +from rasa.engine.storage.storage import ModelStorage from rasa.shared.core.events import BotUttered, SlotSet, UserUttered from rasa.shared.core.flows.flow import ( CollectInformationFlowStep, @@ -62,6 +69,32 @@ def flows(self) -> FlowsList: """ ) + @pytest.fixture(scope="session") + def resource(self) -> Resource: + return Resource(uuid.uuid4().hex) + + @pytest.fixture(scope="session") + def model_storage(self, tmp_path_factory: TempPathFactory) -> ModelStorage: + return LocalModelStorage(tmp_path_factory.mktemp(uuid.uuid4().hex)) + + async def test_llm_command_generator_prompt_init_custom( + self, model_storage: ModelStorage, resource: Resource + ) -> None: + generator = LLMCommandGenerator( + {"prompt": "data/test_prompt_templates/test_prompt.jinja2"}, + model_storage, + resource, + ) + assert generator.prompt_template.startswith("This is a test prompt.") + + async def test_llm_command_generator_prompt_init_default( + self, model_storage: ModelStorage, resource: Resource + ) -> None: + generator = LLMCommandGenerator({}, model_storage, resource) + assert generator.prompt_template.startswith( + "Your task is to analyze the current conversation" + ) + def test_predict_commands_with_no_flows( self, command_generator: LLMCommandGenerator ): diff --git a/tests/dialogues.py b/tests/dialogues.py index 412326cc3baf..d2deefb3559c 100644 --- a/tests/dialogues.py +++ b/tests/dialogues.py @@ -272,6 +272,6 @@ TEST_DOMAINS_FOR_DIALOGUES = [ "data/test_domains/default_with_slots.yml", - "examples/formbot/domain.yml", + "examples/nlu_based/formbot/domain.yml", "data/test_moodbot/domain.yml", ] diff --git a/tests/examples/test_example_bots_training_data.py b/tests/examples/test_example_bots_training_data.py index f5790582afb1..e77b7df48990 100644 --- a/tests/examples/test_example_bots_training_data.py +++ b/tests/examples/test_example_bots_training_data.py @@ -14,23 +14,23 @@ "config_file, domain_file, data_folder, raise_slot_warning, msg", [ ( - "examples/concertbot/config.yml", - "examples/concertbot/domain.yml", - "examples/concertbot/data", + "examples/nlu_based/concertbot/config.yml", + "examples/nlu_based/concertbot/domain.yml", + "examples/nlu_based/concertbot/data", True, None, ), ( - "examples/formbot/config.yml", - "examples/formbot/domain.yml", - "examples/formbot/data", + "examples/nlu_based/formbot/config.yml", + "examples/nlu_based/formbot/domain.yml", + "examples/nlu_based/formbot/data", True, None, ), ( - "examples/knowledgebasebot/config.yml", - "examples/knowledgebasebot/domain.yml", - "examples/knowledgebasebot/data", + "examples/nlu_based/knowledgebasebot/config.yml", + "examples/nlu_based/knowledgebasebot/domain.yml", + "examples/nlu_based/knowledgebasebot/data", True, "You are using an experimental feature: " "Action 'action_query_knowledge_base'!", @@ -43,16 +43,16 @@ None, ), ( - "examples/reminderbot/config.yml", - "examples/reminderbot/domain.yml", - "examples/reminderbot/data", + "examples/nlu_based/reminderbot/config.yml", + "examples/nlu_based/reminderbot/domain.yml", + "examples/nlu_based/reminderbot/data", True, None, ), ( - "examples/rules/config.yml", - "examples/rules/domain.yml", - "examples/rules/data", + "examples/nlu_based/rules/config.yml", + "examples/nlu_based/rules/domain.yml", + "examples/nlu_based/rules/data", True, None, ), diff --git a/tests/graph_components/providers/test_domain_for_core_training_provider.py b/tests/graph_components/providers/test_domain_for_core_training_provider.py index 52e5b7eecb32..0f08ab6b921a 100644 --- a/tests/graph_components/providers/test_domain_for_core_training_provider.py +++ b/tests/graph_components/providers/test_domain_for_core_training_provider.py @@ -98,7 +98,7 @@ def test_train_core_with_original_or_provided_domain_and_compare( default_execution_context: ExecutionContext, ): # Choose an example where the provider will remove a lot of information: - example = Path("examples/formbot/") + example = Path("examples/nlu_based/formbot/") training_files = [example / "data" / "rules.yml"] # Choose a configuration with a policy diff --git a/tests/graph_components/providers/test_rule_only_provider.py b/tests/graph_components/providers/test_rule_only_provider.py index 98100f36a85c..19393fbf3ede 100644 --- a/tests/graph_components/providers/test_rule_only_provider.py +++ b/tests/graph_components/providers/test_rule_only_provider.py @@ -13,8 +13,10 @@ def test_provide( ): resource = Resource("some resource") - domain = Domain.load("examples/rules/domain.yml") - trackers = rasa.core.training.load_data("examples/rules/data/rules.yml", domain) + domain = Domain.load("examples/nlu_based/rules/domain.yml") + trackers = rasa.core.training.load_data( + "examples/nlu_based/rules/data/rules.yml", domain + ) policy = RulePolicy.create( RulePolicy.get_default_config(), diff --git a/tests/shared/core/test_domain.py b/tests/shared/core/test_domain.py index 08e8164ef81f..e6c313f07f42 100644 --- a/tests/shared/core/test_domain.py +++ b/tests/shared/core/test_domain.py @@ -23,6 +23,7 @@ from rasa.shared.core.slots import InvalidSlotTypeException, TextSlot from rasa.shared.core.constants import ( DEFAULT_INTENTS, + KNOWLEDGE_BASE_SLOT_NAMES, SLOT_LISTED_ITEMS, SLOT_LAST_OBJECT, SLOT_LAST_OBJECT_TYPE, @@ -2371,3 +2372,12 @@ def test_domain_with_slots_without_mappings(caplog: LogCaptureFixture) -> None: "Slot 'slot_without_mappings' has no mappings defined. " "We will continue with an empty list of mappings." ) in caplog.text + + +def test_domain_default_slots_are_marked_as_builtin(domain: Domain) -> None: + all_default_slot_names = DEFAULT_SLOT_NAMES.union(KNOWLEDGE_BASE_SLOT_NAMES) + domain_default_slots = [ + slot for slot in domain.slots if slot.name in all_default_slot_names + ] + + assert all(slot.is_builtin for slot in domain_default_slots) diff --git a/tests/shared/core/test_slot_mappings.py b/tests/shared/core/test_slot_mappings.py index 07be20e270e2..9cb2cd861d88 100644 --- a/tests/shared/core/test_slot_mappings.py +++ b/tests/shared/core/test_slot_mappings.py @@ -28,7 +28,7 @@ def test_slot_mapping_entity_is_desired(slot_name: Text, expected: bool): def test_slot_mapping_intent_is_desired(domain: Domain): - domain = Domain.from_file("examples/formbot/domain.yml") + domain = Domain.from_file("examples/nlu_based/formbot/domain.yml") tracker = DialogueStateTracker("sender_id_test", slots=domain.slots) event1 = UserUttered( text="I'd like to book a restaurant for 2 people.", diff --git a/tests/shared/core/test_slots.py b/tests/shared/core/test_slots.py index 25744b2cc878..efd686e48b17 100644 --- a/tests/shared/core/test_slots.py +++ b/tests/shared/core/test_slots.py @@ -156,6 +156,10 @@ def test_slot_fingerprint_uniqueness( f2 = slot.fingerprint() assert f1 != f2 + def test_slot_is_not_builtin_by_default(self, mappings: List[Dict[Text, Any]]): + slot = self.create_slot(mappings, influence_conversation=False) + assert not slot.is_builtin + class TestTextSlot(SlotTestCollection): def create_slot( diff --git a/tests/shared/utils/test_validation.py b/tests/shared/utils/test_validation.py index c38f8230baea..e9e35b26a793 100644 --- a/tests/shared/utils/test_validation.py +++ b/tests/shared/utils/test_validation.py @@ -4,6 +4,7 @@ import pytest from pep440_version_utils import Version +from rasa.shared.core.flows.yaml_flows_io import FLOWS_SCHEMA_FILE from rasa.shared.exceptions import YamlException, SchemaValidationError import rasa.shared.utils.io @@ -16,7 +17,10 @@ LATEST_TRAINING_DATA_FORMAT_VERSION, ) from rasa.shared.nlu.training_data.formats.rasa_yaml import NLU_SCHEMA_FILE -from rasa.shared.utils.validation import KEY_TRAINING_DATA_FORMAT_VERSION +from rasa.shared.utils.validation import ( + KEY_TRAINING_DATA_FORMAT_VERSION, + validate_yaml_with_jsonschema, +) @pytest.mark.parametrize( @@ -380,3 +384,297 @@ def validate() -> None: thread.join() assert len(successful_results) == len(threads) + + +@pytest.mark.parametrize( + "flow_yaml", + [ + """flows: + replace_eligible_card: + description: Never predict StartFlow for this flow, users are not able to trigger. + name: replace eligible card + steps: + - collect: replacement_reason + next: + - if: replacement_reason == "lost" + then: + - collect: was_card_used_fraudulently + ask_before_filling: true + next: + - if: was_card_used_fraudulently + then: + - action: utter_report_fraud + next: END + - else: start_replacement + - if: "replacement_reason == 'damaged'" + then: start_replacement + - else: + - action: utter_unknown_replacement_reason_handover + next: END + - id: start_replacement + action: utter_will_cancel_and_send_new + - action: utter_new_card_has_been_ordered""", + """flows: + replace_card: + description: The user needs to replace their card. + name: replace_card + steps: + - collect: confirm_correct_card + ask_before_filling: true + next: + - if: "confirm_correct_card" + then: + - link: "replace_eligible_card" + - else: + - action: utter_relevant_card_not_linked + next: END + """, + f""" +version: "{LATEST_TRAINING_DATA_FORMAT_VERSION}" +flows: + transfer_money: + description: This flow lets users send money. + name: transfer money + steps: + - id: "ask_recipient" + collect: transfer_recipient + next: "ask_amount" + - id: "ask_amount" + collect: transfer_amount + next: "execute_transfer" + - id: "execute_transfer" + action: action_transfer_money""", + """flows: + setup_recurrent_payment: + name: setup recurrent payment + steps: + - collect: recurrent_payment_type + rejections: + - if: not ({"direct debit" "standing order"} contains recurrent_payment_type) + utter: utter_invalid_recurrent_payment_type + description: the type of payment + - collect: recurrent_payment_recipient + utter: utter_ask_recipient + description: the name of a person + - collect: recurrent_payment_amount_of_money + description: the amount of money without any currency designation + - collect: recurrent_payment_frequency + description: the frequency of the payment + rejections: + - if: not ({"monthly" "yearly"} contains recurrent_payment_frequency) + utter: utter_invalid_recurrent_payment_frequency + - collect: recurrent_payment_start_date + description: the start date of the payment + - collect: recurrent_payment_end_date + description: the end date of the payment + rejections: + - if: recurrent_payment_end_date < recurrent_payment_start_date + utter: utter_invalid_recurrent_payment_end_date + - collect: recurrent_payment_confirmation + description: accepts True or False + ask_before_filling: true + next: + - if: not recurrent_payment_confirmation + then: + - action: utter_payment_cancelled + next: END + - else: "execute_payment" + - id: "execute_payment" + action: action_execute_recurrent_payment + next: + - if: setup_recurrent_payment_successful + then: + - action: utter_payment_complete + next: END + - else: "payment_failed" + - id: "payment_failed" + action: utter_payment_failed + - action: utter_failed_payment_handover + - action: utter_failed_handoff""", + """ + flows: + foo_flow: + steps: + - id: "1" + set_slots: + - foo: bar + next: "2" + - id: "2" + action: action_listen + next: "1" + """, + """ + flows: + test_flow: + description: Test flow + steps: + - id: "1" + action: action_xyz + next: "2" + - id: "2" + action: utter_ask_name""", + ], +) +def test_flow_validation_pass(flow_yaml: str) -> None: + # test fails if exception is raised + validate_yaml_with_jsonschema(flow_yaml, FLOWS_SCHEMA_FILE) + + +@pytest.mark.parametrize( + "flow_yaml, error_msg", + [ + ("""flows:""", "None is not of type 'object'."), + ( + """flows: + test: + name: test + steps:""", + ("None is not of type 'array'."), + ), + ( + """flows: + test: + - id: test""", + "[ordereddict([('id', 'test')])] is not of type 'object'.", + ), + ( + """flows: + test: + name: test + steps: + - collect: recurrent_payment_type + rejections: + - if: not ({"direct debit" "standing order"} contains recurrent_payment_type) + utter: utter_invalid_recurrent_payment_type + desc: the type of payment""", + ( + "('desc', 'the type of payment')]) is not valid" + " under any of the given schemas." + ), + ), + ( # next is a Bool + """flows: + test: + name: test + steps: + - collect: confirm_correct_card + ask_before_filling: true + next: + - if: "confirm_correct_card" + then: + - link: "replace_eligible_card" + - else: + - action: utter_relevant_card_not_linked + next: True""", + "('next', True)])])])])]) is not valid under any of the given schemas.", + ), + ( # just next and ask_before_filling + """flows: + test: + name: test + steps: + - ask_before_filling: true + next: + - if: "confirm_correct_card" + then: + - link: "replace_eligible_card" + - else: + - action: utter_relevant_card_not_linked + next: END""", + ( + "('if', 'confirm_correct_card'), ('then'," + " [ordereddict([('link', 'replace_eligible_card')])])]), " + "ordereddict([('else', [ordereddict([('action', " + "'utter_relevant_card_not_linked'), ('next', 'END')])])])]" + " is not of type 'null'. Failed to validate data," + " make sure your data is valid." + ), + ), + ( # action added to collect + """flows: + test: + steps: + - collect: confirm_correct_card + action: utter_xyz + ask_before_filling: true""", + ( + "([('collect', 'confirm_correct_card'), ('action', 'utter_xyz')," + " ('ask_before_filling', True)])" + " is not valid under any of the given schemas." + ), + ), + ( # random addition to action + """flows: + test: + steps: + - action: utter_xyz + random_xyz: true + next: END""", + "Failed validating 'type' in schema[2]['properties']['next']", + ), + ( # random addition to collect + """flows: + test: + steps: + - collect: confirm_correct_card + random_xyz: utter_xyz + ask_before_filling: true""", + ( + "ordereddict([('collect', 'confirm_correct_card'), " + "('random_xyz', 'utter_xyz'), ('ask_before_filling', True)])" + " is not valid under any of the given schemas." + ), + ), + ( # random addition to flow definition + """flows: + test: + random_xyz: True + steps: + - action: utter_xyz + next: id-21312""", + "Additional properties are not allowed ('random_xyz' was unexpected).", + ), + ( + """flows: + test: + steps: + - action: True + next: id-2132""", + ( + "ordereddict([('action', True), ('next', 'id-2132')])" + " is not valid under any of the given schemas." + ), + ), + ( # next is a step + """flows: + test: + steps: + - action: xyz + next: + - action: utter_xyz""", + ( + "([('action', 'xyz'), ('next'," + " [ordereddict([('action', 'utter_xyz')])])])" + " is not valid under any of the given schemas." + ), + ), + ( # next is without then + """flows: + test: + steps: + - action: xyz + next: + - if: xyz""", + ( + "([('action', 'xyz'), ('next', [ordereddict([('if', 'xyz')])])])" + " is not valid under any of the given schemas." + ), + ), + ], +) +def test_flow_validation_fail(flow_yaml: str, error_msg: str) -> None: + with pytest.raises(SchemaValidationError) as e: + rasa.shared.utils.validation.validate_yaml_with_jsonschema( + flow_yaml, FLOWS_SCHEMA_FILE + ) + assert error_msg in str(e.value) diff --git a/tests/test_validator.py b/tests/test_validator.py index 7b0e9ef54b9d..9d5207aa3cf6 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -348,7 +348,7 @@ def test_early_exit_on_invalid_domain(): def test_verify_there_is_not_example_repetition_in_intents(): importer = RasaFileImporter( domain_path="data/test_moodbot/domain.yml", - training_data_paths=["examples/knowledgebasebot/data/nlu.yml"], + training_data_paths=["examples/nlu_based/knowledgebasebot/data/nlu.yml"], ) validator = Validator.from_importer(importer) # force validator to not ignore warnings (default is True) @@ -1158,13 +1158,10 @@ def test_verify_unique_flows_duplicate_names( ) in caplog.text -# testing for None and empty string -@pytest.mark.parametrize("way_of_emptiness", ["", '""']) def test_verify_flow_names_non_empty( tmp_path: Path, nlu_data_path: Path, caplog: LogCaptureFixture, - way_of_emptiness: str, ) -> None: flows_file = tmp_path / "flows.yml" with open(flows_file, "w") as file: @@ -1174,7 +1171,7 @@ def test_verify_flow_names_non_empty( flows: transfer_money: description: This flow lets users send money. - name: {way_of_emptiness} + name: "" steps: - collect: transfer_recipient """