Deactivate the form for RASA 2.0

Hi i am using rasa open source:

Rasa Version : 2.8.12 Minimum Compatible Version: 2.8.9 Rasa SDK Version : 2.8.1 Rasa X Version : None Python Version : 3.8.0 Operating System : Windows-10-10.0.19041-SP0

i want deactivate the form after the user enter software that is not listed in standard_software. instead of keep on asking the next required_slot the bot should just send the link to user fill in the website but it keep on asking the next slot to fill in by the user. how do i fixed this?

from logging import Logger
from typing import Any, Text, Dict, List, Union, Optional

from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.events import SlotSet, AllSlotsReset, EventType
from rasa_sdk.forms import REQUESTED_SLOT, FormValidationAction

import re
from rasa_sdk.interfaces import ActionExecutionRejection

from rasa_sdk.types import DomainDict

from actions.utils import validate_email_address, validate_asset_tag_no

class ValidateSoftwareInstallationForm(FormValidationAction):
  """Form action to raise a software installation request"""

  #TODO: Implement lookup
  standard_software = [
    "MS Office", 
    "MS Project",
    "MS Visio",
    "Acrobat Writer",
    "IBM Personal Communications",
    "Acrobat Reader",
    "Java 6",
    "Google Chrome",
    "Lotus Notes 6.5.3",
    "Webex",
    "7 Zip",
    "SAPGUI",
    "FireEye",
    "Zoom Apps",
    "VMware Horizon Client",
    "TeamMate application",
    "Citrix Netscaler Gateaway"
  ]

  def name(self) -> Text:
    """Unique identifier of the form"""

    return "validate_software_installation_form"

  @staticmethod
  async def required_slots(
        self,
        #slots_mapped_in_domain: List[Text],
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: "DomainDict",
    ) -> Optional[List[Text]]:
      
    
    required_slots = [
      "software",
      "location",
      "purpose",
      "asset_tag",
      "approver",
      # "department"
    ]

   
    if tracker.get_slot("req_subject") == "other":
      required_slots.insert(0,"pf_number")

    if tracker.get_slot("software") in ValidateSoftwareInstallationForm.standard_software:
      required_slots.append("cost_centre")
    else:
      #required_slots.extend([
        #"soft_background",
        #"usage_duration"
       # ])        

    return required_slots

  def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict]]]:
    """A dictionary to map required slots to
      - an extracted entity
      - intent: value pairs
      - a whole message
      or a list of them, where a first match will be picked"""
    return {
      "software": [
          self.from_entity(entity="software", intent="install_software"),
          self.from_entity(entity="software", intent="inform"),
      ],
      "pf_number": [
          #self.from_entity(entity="pf_number", intent="inform"),
          self.from_entity(entity="pf_number", intent="inform"),
      ],
      # "department": [
      #     self.from_entity(entity="department", intent="inform"),
      # ],
      "location": [
          self.from_entity(entity="location", intent="inform"),
          #self.from_entity(entity="location"),
          #self.from_text(),
      ],
      "purpose": self.from_entity(entity="purpose", intent="inform"),
      "asset_tag": self.from_entity(entity="asset_tag", intent="inform"),
      "approver": [
        self.from_entity(entity="email", intent="inform"),
        #self.from_text(),
      ],
      "soft_background": self.from_entity(entity="soft_background", intent="inform"),
      "usage_duration": self.from_entity(entity="usage_duration", intent="inform"),
      "cost_centre": self.from_entity(entity="cost_centre", intent="inform"),

    }     
  
  def request_next_slot(
    self,
    dispatcher: "CollectingDispatcher",
    tracker: "Tracker",
    domain: Dict[Text, Any],
  ) -> Optional[List[EventType]]:
    """Request the next slot and utter template if needed, else return None"""

    for slot in self.required_slots(tracker):
      if self._should_request_slot(tracker, slot):

        # Show the standard software list
        if slot == "software":
        

          buttons = []
          
          for software in ValidateSoftwareInstallationForm.standard_software:
            payload = "/inform{\"software\":\"" + software + "\"}"
            buttons.append(
              #{"title": software, "payload": payload}
              {"title":software, "payload": payload}
            )
  
          message = "What software would you like to install? " + \
            "Please choose from this list of standard software or key in your non-standard software."

          
          #super.logger.debug("Requesting software slot")
          dispatcher.utter_message(
            text=message,
            buttons=buttons)
        else:
          #super.logger.debug(f"Request next slot '{slot}'")
          dispatcher.utter_message(template=f"utter_ask_{slot}", **tracker.slots)
        
        return [SlotSet("requested_slot", slot)]

    # no more required slots to fill
    return None

  @staticmethod
  def is_int(string: Text) -> bool:
    """Check if a string is an integer"""

    try:
      int(string)
      return True
    except ValueError:
      return False

  def validate_software(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
  ) -> Dict[Text, Any]:
    "Display non-standard software info"
    if value not in ValidateSoftwareInstallationForm.standard_software:
      message = "As your requested software is not in the standard usage list, " + \
        "please submit your request on the website"
      dispatcher.utter_message(text=message)
    
    return {"software": value}
    #return self.deactivate()

  def validate_pf_number(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
  ) -> Dict[Text, Any]:
    """Validate PF number."""
    #print("Check if " + value + " is valid PF.")
    if self.is_int(value) and len(value) == 8:
      # validation succeeded, set the value of the "cuisine" slot to value
      #print("PF number valid")
      return {"pf_number": value}
    else:
      dispatcher.utter_message(template="utter_wrong_pf_format")
      # validation failed, set this slot to None, meaning the
      # user will be asked for the slot again
      return {"pf_number": None}
  
  def validate_asset_tag(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
  ) -> Dict[Text, Any]:
    """Validate asset tag number"""
    if validate_asset_tag_no(value=value, dispatcher=dispatcher):
      return {"asset_tag": value}
    else:
      return {"asset_tag": None}

  def validate_approver(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
  ) -> Dict[Text, Any]:
    """Check that approver email is valid email address"""

    if validate_email_address(email=value, dispatcher=dispatcher):
      return {"approver": value}
    else:
      return {"approver": None}
    # #TODO: Check with People Directory if approver is valid

    # email_regex = '^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$'
    # if re.search(email_regex,value):
    #   return {"approver": value}
    # else:
    #   dispatcher.utter_message(text="Sorry, that is not a valid email address. Please try again.")
    #   return {"approver": None}

  def submit(
    self,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
  ) -> List[Dict]:
    """Define what the form has to do
        after all required slots are filled"""
   
    
    slots = tracker.current_slot_values()
    software = slots['software']
    location = slots['location']
    purpose = slots['purpose']
    asset_tag = slots['asset_tag']
    approver = slots['approver']
    
    dispatcher.utter_message("Here are your ticket details.")
    dispatcher.utter_message("Software: " + software)
    dispatcher.utter_message("Purpose: " + purpose)
    dispatcher.utter_message("Asset tag: " + asset_tag)
    dispatcher.utter_message("Location: " + location)
    dispatcher.utter_message("Approver Email: " + approver)
    
    buttons = [
      {"title": "Yes", "payload": "/affirm"},
      {"title": "No", "payload": "/deny"}
    ]
    dispatcher.utter_message(text="Shall I submit?", buttons=buttons)

    return []


Hi, you are almost there!

One way to deactivate a form is to reject the slot value in your validation action. This triggers an event called a ActionExecutionRejection, allowing the rule to take over.

Alternatively, you can also return the ActionExecutionRejection action (see here for more details)

You need to invalidate the slot, as in: set it to None. I would also set requested_slot to None because you can’t write a rule based on the intents (since the intent may be inform, which is not a unique intent to this situation), so the rule needs to trigger this based on a slot. We can discuss other ways to do this, if it’s helpful.

  def validate_software(
    self,
    value: Text,
    dispatcher: CollectingDispatcher,
    tracker: Tracker,
    domain: Dict[Text, Any],
  ) -> Dict[Text, Any]:
    "Display non-standard software info"
    if value not in ValidateSoftwareInstallationForm.standard_software:
      message = "As your requested software is not in the standard usage list, " + \
        "please submit your request on the website"
      dispatcher.utter_message(text=message)
      return {"software": None, "requested_slot": None}
    return {"software": value}

And also add a rule for ending the form on requested_slot set to None:

rules:
- rule: Invalid software
  condition:
  - active_loop: software_installation_form
  steps:
  - intent: install_software
  - active_loop: null
  - slot_was_set:
     - requested_slot: null
  - action: <whatever should happen here>

You can see an example of this in our financial-demo starter pack. The action is here, the rule is here. For the financial-demo assistant, we also defined a followup action here, you could optionally do this, or handle the behaviour with the dispatcher, as you’ve done so far.

2 Likes