Repository: RasaHQ/rasa
Pull request: updated formbot and docs
**Proposed changes**:
- fixes #3176 and 1422 (issue in old core)
- Form validation method is used as intended now and with `validate_{requested_slot}`

**Status (please check what you already did)**:
- [x] made PR ready for code review
- [ ] added some tests for the functionality
- [x] updated the documentation
- [ ] updated the changelog


Problem statement:
update form docs
Explain what every line in the story does for the form and when what information is needed in the stories. 
For example, the stories shown in the docs include: `- form{"name": null}` but don't say what it is doing. Also since interactive learning is printing much more than required that could be explained.

We want to to find the `forms docs` by searching `forms` and not just when searching `formaction`

Hints from issue discussion:

Commit message: updated formbot and docs

Patch to review:
diff --git a/docs/core/slotfilling.rst b/docs/core/slotfilling.rst
index 7582a325d72a..33bcf674b8dc 100644
--- a/docs/core/slotfilling.rst
+++ b/docs/core/slotfilling.rst
@@ -37,8 +37,8 @@ under ``forms:`` section:
 
 .. _section_form_basics:
 
-Basics
-------
+Forms Basics
+------------
 
 Using a ``FormAction``, you can describe *all* of the happy paths with a single
 story. By "happy path", we mean that whenever you ask a user for some information,
@@ -55,6 +55,14 @@ happy paths.
         - form{"name": "restaurant_form"}
         - form{"name": null}
 
+In this story the user intent is ``request_restaurant``, which is followed by
+the form action ``restaurant_form``. With ``form{"name": "restaurant_form"}`` the
+form is activated and with ``form{"name": null}`` the form is deactivated again.
+As shown in the section :ref:`section_unhappy` the the bot can execute any kind of
+actions outside the form while the form is still active, but on the "happy path",
+where the user is cooperating well and the system understands the user input correctly,
+the form is filling all requested slots without interruption.
+
 The ``FormAction`` will only requests slots which haven't already been set.
 If a user says
 "I'd like a vegetarian Chinese restaurant for 8 people", they won't be
@@ -159,7 +167,7 @@ Validating user input
 After extracting a slot value from user input, the form will try to validate the
 value of the slot. By default, this only checks if the requested slot was extracted.
 If you want to add custom validation, for example to check a value against a database,
-you can do this by overwriting the ``validate()`` method.
+you can do this by writing ``validate_{slot}()`` method.
 Here is an example which checks if the extracted cuisine slot belongs to a
 list of supported cuisines.
 
@@ -167,10 +175,7 @@ list of supported cuisines.
    :pyobject: RestaurantForm.cuisine_db
 
 .. literalinclude:: ../../examples/formbot/actions.py
-   :pyobject: RestaurantForm.is_int
-
-.. literalinclude:: ../../examples/formbot/actions.py
-   :pyobject: RestaurantForm.validate
+   :pyobject: RestaurantForm.validate_cuisine
 
 You can also deactivate the form directly during this validation step (in case the
 slot is filled with something that you are certain can't be handled) by returning
@@ -303,7 +308,7 @@ kinds of logic into your forms.
 Configuration
 -------------
 To use forms, make sure to include the ``FormPolicy`` in your policy
-configuration file. For exmple:
+configuration file. For example:
 
 .. code-block:: yaml
 
diff --git a/examples/formbot/actions.py b/examples/formbot/actions.py
index 264fe2f5b7a2..39a27de7e4d7 100644
--- a/examples/formbot/actions.py
+++ b/examples/formbot/actions.py
@@ -1,18 +1,15 @@
 # -*- coding: utf-8 -*-
-from typing import Dict, Text, Any, List, Union
+from typing import Dict, Text, Any, List, Union, Optional
 
-from rasa_core_sdk import ActionExecutionRejection
 from rasa_core_sdk import Tracker
-from rasa_core_sdk.events import SlotSet
 from rasa_core_sdk.executor import CollectingDispatcher
-from rasa_core_sdk.forms import FormAction, REQUESTED_SLOT
+from rasa_core_sdk.forms import FormAction
 
 
 class RestaurantForm(FormAction):
     """Example of a custom form action"""
 
-    def name(self):
-        # type: () -> Text
+    def name(self) -> Text:
         """Unique identifier of the form"""
 
         return "restaurant_form"
@@ -24,8 +21,7 @@ def required_slots(tracker: Tracker) -> List[Text]:
         return ["cuisine", "num_people", "outdoor_seating",
                 "preferences", "feedback"]
 
-    def slot_mappings(self):
-        # type: () -> Dict[Text: Union[Dict, List[Dict]]]
+    def slot_mappings(self) -> Dict[Text: Union[Dict, List[Dict]]]:
         """A dictionary to map required slots to
             - an extracted entity
             - intent: value pairs
@@ -51,9 +47,9 @@ def slot_mappings(self):
                              self.from_text()]}
 
     @staticmethod
-    def cuisine_db():
-        # type: () -> List[Text]
+    def cuisine_db() -> List[Text]:
         """Database of supported cuisines"""
+
         return ["caribbean",
                 "chinese",
                 "french",
@@ -63,72 +59,68 @@ def cuisine_db():
                 "mexican"]
 
     @staticmethod
-    def is_int(string: Text) -> bool:
+    def is_int(string: Text) -> Any:
         """Check if a string is an integer"""
+
         try:
             int(string)
             return True
         except ValueError:
             return False
 
-    def validate(self,
-                 dispatcher: CollectingDispatcher,
-                 tracker: Tracker,
-                 domain: Dict[Text, Any]) -> List[Dict]:
-        """Validate extracted requested slot
-            else reject the execution of the form action
-        """
-        # extract other slots that were not requested
-        # but set by corresponding entity
-        slot_values = self.extract_other_slots(dispatcher, tracker, domain)
-
-        # extract requested slot
-        slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
-        if slot_to_fill:
-            slot_values.update(self.extract_requested_slot(dispatcher,
-                                                           tracker, domain))
-            if not slot_values:
-                # reject form action execution
-                # if some slot was requested but nothing was extracted
-                # it will allow other policies to predict another action
-                raise ActionExecutionRejection(self.name(),
-                                               "Failed to validate slot {0} "
-                                               "with action {1}"
-                                               "".format(slot_to_fill,
-                                                         self.name()))
-
-        # we'll check when validation failed in order
-        # to add appropriate utterances
-        for slot, value in slot_values.items():
-            if slot == 'cuisine':
-                if value.lower() not in self.cuisine_db():
-                    dispatcher.utter_template('utter_wrong_cuisine', tracker)
-                    # validation failed, set slot to None
-                    slot_values[slot] = None
-
-            elif slot == 'num_people':
-                if not self.is_int(value) or int(value) <= 0:
-                    dispatcher.utter_template('utter_wrong_num_people',
-                                              tracker)
-                    # validation failed, set slot to None
-                    slot_values[slot] = None
-
-            elif slot == 'outdoor_seating':
-                if isinstance(value, str):
-                    if 'out' in value:
-                        # convert "out..." to True
-                        slot_values[slot] = True
-                    elif 'in' in value:
-                        # convert "in..." to False
-                        slot_values[slot] = False
-                    else:
-                        dispatcher.utter_template('utter_wrong_outdoor_seating',
-                                                  tracker)
-                        # validation failed, set slot to None
-                        slot_values[slot] = None
-
-        # validation succeed, set the slots values to the extracted values
-        return [SlotSet(slot, value) for slot, value in slot_values.items()]
+    def validate_cuisine(self,
+                         value: Text,
+                         dispatcher: CollectingDispatcher,
+                         tracker: Tracker,
+                         domain: Dict[Text, Any]) -> Optional[Text]:
+        """Validate cuisine value."""
+
+        if value.lower() in self.cuisine_db():
+            # validation succeeded
+            return value
+        else:
+            dispatcher.utter_template('utter_wrong_cuisine', tracker)
+            # validation failed, set this slot to None, meaning the
+            # user will be asked for the slot again
+            return None
+
+    def validate_num_people(self,
+                            value: Text,
+                            dispatcher: CollectingDispatcher,
+                            tracker: Tracker,
+                            domain: Dict[Text, Any]) -> Optional[Text]:
+        """Validate num_people value."""
+
+        if not self.is_int(value) or int(value) <= 0:
+            dispatcher.utter_template('utter_wrong_num_people', tracker)
+            # validation failed, set slot to None
+            return None
+        else:
+            return value
+
+    @staticmethod
+    def validate_outdoor_seating(value: Text,
+                                 dispatcher: CollectingDispatcher,
+                                 tracker: Tracker,
+                                 domain: Dict[Text, Any]) -> Any:
+        """Validate outdoor_seating value."""
+
+        if isinstance(value, str):
+            if 'out' in value:
+                # convert "out..." to True
+                return True
+            elif 'in' in value:
+                # convert "in..." to False
+                return False
+            else:
+                dispatcher.utter_template('utter_wrong_outdoor_seating',
+                                          tracker)
+                # validation failed, set slot to None
+                return None
+
+        else:
+            # affirm/deny was picked up as T/F
+            return value
 
     def submit(self,
                dispatcher: CollectingDispatcher,


Code review comments:
diff_hunk: @@ -63,72 +59,68 @@ def cuisine_db():
                 "mexican"]
 
     @staticmethod
-    def is_int(string: Text) -> bool:
+    def is_int(string: Text) -> Any:
         """Check if a string is an integer"""
+
         try:
             int(string)
             return True
         except ValueError:
             return False
 
-    def validate(self,
-                 dispatcher: CollectingDispatcher,
-                 tracker: Tracker,
-                 domain: Dict[Text, Any]) -> List[Dict]:
-        """Validate extracted requested slot
-            else reject the execution of the form action
-        """
-        # extract other slots that were not requested
-        # but set by corresponding entity
-        slot_values = self.extract_other_slots(dispatcher, tracker, domain)
-
-        # extract requested slot
-        slot_to_fill = tracker.get_slot(REQUESTED_SLOT)
-        if slot_to_fill:
-            slot_values.update(self.extract_requested_slot(dispatcher,
-                                                           tracker, domain))
-            if not slot_values:
-                # reject form action execution
-                # if some slot was requested but nothing was extracted
-                # it will allow other policies to predict another action
-                raise ActionExecutionRejection(self.name(),
-                                               "Failed to validate slot {0} "
-                                               "with action {1}"
-                                               "".format(slot_to_fill,
-                                                         self.name()))
-
-        # we'll check when validation failed in order
-        # to add appropriate utterances
-        for slot, value in slot_values.items():
-            if slot == 'cuisine':
-                if value.lower() not in self.cuisine_db():
-                    dispatcher.utter_template('utter_wrong_cuisine', tracker)
-                    # validation failed, set slot to None
-                    slot_values[slot] = None
-
-            elif slot == 'num_people':
-                if not self.is_int(value) or int(value) <= 0:
-                    dispatcher.utter_template('utter_wrong_num_people',
-                                              tracker)
-                    # validation failed, set slot to None
-                    slot_values[slot] = None
-
-            elif slot == 'outdoor_seating':
-                if isinstance(value, str):
-                    if 'out' in value:
-                        # convert "out..." to True
-                        slot_values[slot] = True
-                    elif 'in' in value:
-                        # convert "in..." to False
-                        slot_values[slot] = False
-                    else:
-                        dispatcher.utter_template('utter_wrong_outdoor_seating',
-                                                  tracker)
-                        # validation failed, set slot to None
-                        slot_values[slot] = None
-
-        # validation succeed, set the slots values to the extracted values
-        return [SlotSet(slot, value) for slot, value in slot_values.items()]
+    def validate_cuisine(self,
+                         value: Text,
+                         dispatcher: CollectingDispatcher,
+                         tracker: Tracker,
+                         domain: Dict[Text, Any]) -> Optional[Text]:
+        """Validate cuisine value."""
+
+        if value.lower() in self.cuisine_db():
+            # validation succeeded
+            return value
+        else:
+            dispatcher.utter_template('utter_wrong_cuisine', tracker)
+            # validation failed, set this slot to None, meaning the
+            # user will be asked for the slot again
+            return None
+
+    def validate_num_people(self,
+                            value: Text,
+                            dispatcher: CollectingDispatcher,
+                            tracker: Tracker,
+                            domain: Dict[Text, Any]) -> Optional[Text]:
+        """Validate num_people value."""
+
+        if not self.is_int(value) or int(value) <= 0:
path: examples/formbot/actions.py
text: @user1:
let's also convert to positive if as in cuisine case

diff_hunk: @@ -63,72 +59,68 @@ def cuisine_db():
                 "mexican"]
 
     @staticmethod
-    def is_int(string: Text) -> bool:
+    def is_int(string: Text) -> Any:
path: examples/formbot/actions.py
text: @user1:
why is it not `bool`?