How to input free text with custom actions & slots

RasaCore : 0.14.1 — RasaNLU : 0.15.0 – Python 3.6.7


Before this step, you will need a NLU model (you can follow the first step of this video : https://www.youtube.com/watch?v=xu6D_vLP5vY (in my exemple you will need to train 3 intents (greet,goodbye,ask_find))

Step 1) Then you will need to create 5 files :

-endpoints.yml | -domain.yml | -stories.md | -actions.py | -train_online.py


endpoints.yml

action_endpoint:
  url: "http://localhost:5055/webhook/"

domain.yml

slots:
  recherche:
    type: text
  
intents:
  - greet
  - goodbye
  - ask_find
 

entities:
  - recherche

templates:
  utter_greet:
    - text: "Hey !"
  utter_goodbye:
    - text: "Thk body"
  utter_ask_recherche:
    - text: "Do you need something ?"
  utter_slots_values:
    - text: "What you asked for --> {recherche}"


actions:
  - utter_slots_values
  - utter_greet
  - utter_goodbye
  - utter_ask_recherche
  - action_wiki

stories.md

## Story 1
* greet
    - utter_greet
* ask_find
    - utter_ask_recherche
    - action_listen
    - action_wiki
    - utter_slots_values
* goodbye
    - utter_goodbye

actions.py

# -*- coding: utf-8 -*-
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 import Action

class Action_wiki(Action):
    """Example of a custom action"""

    def name(self):
        # type: () -> Text
        """Unique identifier of the action"""

        return "action_wiki"

    def run(self, dispatcher, tracker, domain):
        message = tracker.latest_message.get('text')
        return [SlotSet('recherche', message)]

train_online.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import logging

from rasa_core.agent import Agent
from rasa_core.policies.keras_policy import KerasPolicy
from rasa_core.policies.memoization import MemoizationPolicy
from rasa_core.interpreter import RasaNLUInterpreter
from rasa_core.training import interactive

from rasa_core.utils import EndpointConfig
from rasa_core.policies.form_policy import FormPolicy

logger = logging.getLogger(__name__)


def run_online(interpreter,
                          domain_file="domain.yml",
                          training_data_file='stories.md'):
    action_endpoint = EndpointConfig(url="http://localhost:5055/webhook")						  
    agent = Agent(domain_file,
                  policies=[MemoizationPolicy(max_history=2), KerasPolicy(max_history=3, epochs=3, batch_size=50), FormPolicy()],
                  interpreter=interpreter,
				  action_endpoint=action_endpoint)
    				  
    data = agent.load_data(training_data_file)			   
    agent.train(data)
    interactive.run_interactive_learning(agent, training_data_file)
    return agent


if __name__ == '__main__':
    logging.basicConfig(level="INFO")
    nlu_interpreter = RasaNLUInterpreter('./Model/default/model_20190502-164014')
    run_online(nlu_interpreter)

Step 2) Open a cmd Terminal where your 5 files are and put this command line :

If you have only python3 → python -m rasa_core_sdk.endpoint --actions actions

  • or if you have python2 & python3 → python3 -m rasa_core_sdk.endpoint --actions actions

Step 3) Run in an other terminal your “train_online.py” file


                                    END

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


For information (in actions.py) the run function take the last user message (listened by the “action_listen” function) and fill the slot named “recherche” (means ‘search’ in french):

def run(self, dispatcher, tracker, domain):
    message = tracker.latest_message.get('text')
    return [SlotSet('recherche', message)]

PS : There is an other method to get free text from user with Forms.

Hope i helped you :slight_smile:

1 Like

Hi isgaal,

I’ve tried your solution because I am developing a chatbot to act as a frontline before people start mailing the helpdesk.

Right now, the application is still young and does not need to actually send emails, I just want it to be able to take free text, put it into a slot and utter a reply to the user telling them their email has been sent. (Only free text, rest of application functionally does not matter.)

I’ve implemented the domain part like this:

slots:
 emailbody:
  type: text

entities:
 - emailbody

actions:
 - utter_email
 - utter_email_askbody
 - utter_email_confirm
     
 - action_mail
intents:
 - email

templates:
 utter_menu_2:
 - text: Hier is deel 2 van het menu.
   buttons:
    - title: Versturen van een fax via Outlook.
      payload: /outlookfax
    - title: Toegang tot WiFi "Internet-Access"
      payload: /internalwifi
    - title: Toegang tot WiFi "Guest-Access"
      payload: /guestwifi
    - title: Nieuwe telefoon, hoe inloggen?
      payload: /newphone
    - title: Nieuwe telefoon, "features"?
      payload: /newphonefeatures
    - title: Deel 1 van de opties bekijken.
      payload: /menu
  
    - title: Email versturen naar helpdesk.
      payload: /email
  
  
  
  
 utter_email:
 - text: Heeft u graag dat ik een email opstel naar de helpdesk voor u?
   buttons:
    - title: Ja
      payload: /email
    - title: Nee
      payload: /help
  
 utter_email_askbody:
 - text: U mag de inhoud van de mail hier typen, als u klaar bent duwt u op verzending via ENTER
 
 
 utter_email_confirm:
 - text: Uw email is opgeslagen en verzonden naar de helpdesk.

I know it is in Dutch, but the last buttons configured with payload: /email are the ones triggering the email path in stories.md:

##path_email
* email
 - utter_email_askbody
 - action_listen
 - action_mail
 - utter_email_confirm
 - utter_welcome
* thankyou
 - utter_thankyou

Then action_mail in actions.py looks like this: (I’ve copied all of your imports, I have a custom default_fallback action left out of screen and the endpoints are correctly configured.)

class Action_mail(Action):
	
	def name(self):
		#type: () -> Text
		
		return "action_mail"
		
	def run(self, dispatcher, tracker, domain):
		message = tracker.latest_message.get('text')
		return [SlotSet('emailbody', message)]

The issue is: As soon as I let the user input something after “-utter_email_askbody” in stories the model tries to classify the intent and logically goes to its fallback action due to not being able to classify the free text like this: (the blue arrow indicates the free text I put in)

Is there something I’m missing?

Hi @Heindrich,

We will try something else → we will use a Form to do that.

Forms will be better for your applications because they won’t trigger the falback. If you want to know more about forms : https://rasa.com/docs/core/slotfilling/#forms-basics


actions.py

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


class emailForm(FormAction):
    """Example of a custom form action"""

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

        return "email_form"

    @staticmethod
    def required_slots(tracker: Tracker) -> List[Text]:
        """A list of required slots that the form has to fill"""

        return ["emailbody"]

    def slot_mappings(self):
        # type: () -> 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 {"emailbody": [self.from_text()]}

    
    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"""

        # utter submit template
        dispatcher.utter_template('utter_submit', tracker)
        return []

domain.yml (this is an exemple you will have to add your modifications)

slots:
  emailbody:
    type: unfeaturized
    


intents:
  - greet
  - goodbye
  - ask_email
 

entities:
  - email

templates:
  utter_greet:
    - text: "hey !"
  utter_goodbye:
    - text: "Bye !"
  utter_ask_recherche:
    - text: "please, give me your e-mail"
  utter_slots_values:
    - text: "Your e-mail is --> {emailbody}"
  utter_submit:
    - text: "FormCompleted"

actions:
  - utter_slots_values
  - utter_greet
  - utter_submit
  - utter_goodbye
  - utter_ask_recherche

forms:
  - email_form

policies:
  - name: "FormPolicy"

## Story 1
* greet
    - utter_greet
* ask_find
    - utter_ask_recherche
    - email_form
    - form{"name":"email_form"}
    - form{"name": null}
    - utter_submit
    - utter_slots_values
* goodbye
    - utter_goodbye

train_server.py (don’t forget to add “from rasa_core.policies.form_policy import FormPolicy” and in your agent : FormPolicy() (as this exemple)

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import logging
import rasa_core
from rasa_core.agent import Agent
from rasa_core.policies.keras_policy import KerasPolicy
from rasa_core.policies.memoization import MemoizationPolicy
from rasa_core.interpreter import RasaNLUInterpreter
from rasa_core.utils import EndpointConfig
from rasa_core.run import serve_application
from rasa_core import config
from rasa_core.policies.form_policy import FormPolicy

logger = logging.getLogger(__name__)

def train_dialogue(domain_file = 'domain.yml',
					model_path = './models/dialogue1',
					training_data_file = 'stories.md'):
					
	agent = Agent(domain_file, policies = [MemoizationPolicy(), KerasPolicy(max_history=3, epochs=200, batch_size=50),FormPolicy()])
	data = agent.load_data(training_data_file)	
	

	agent.train(data)
				
	agent.persist(model_path)
	return agent
	
def run(serve_forever=True):
	interpreter = RasaNLUInterpreter('./Model/default/YOUR_NLU_MODEL')
	action_endpoint = EndpointConfig(url="http://localhost:5055/webhook")
	agent = Agent.load('./models/dialogueX', interpreter=interpreter, action_endpoint=action_endpoint)
	rasa_core.run.serve_application(agent ,channel='cmdline')
		
	return agent
	
if __name__ == '__main__':
	train_dialogue()
	run()

First of all i did this exemple really quickly, i didn’t tried it, but you have the philosophy. So be carefull if you copy/pasta it. Here you have an exemple from rasa : rasa_core/examples/formbot at master · RasaHQ/rasa_core · GitHub


Also, after that you will have to create an other action to use your slot filled by the form, but i let you this step.

Please, if you found your solution can you please share with us your result, to help other people :slight_smile: ? Good luck, and i hope i helped you.

Hi isgaal,

It worked perfectly! The system is faking a notification after taking in any free mail input and hereby announces the mail being sent in this case.

The only issue is the slot not resetting after the mail has been sent, but I trust this is easily done by just resetting the slot in the return function of the submit function, rather than just return []. Either that or I will create a second action which gets executed right after to reset the email slot.

I will try that next!

Thanks for all of the help, you just made me get closer to passing the deadline for my thesis!

I dont get hows that’s free text ?? your Action_wiki is triggers only when bot finds an input with intent ask_find , you just set slot with latest input . That’s not free text , a free text should be a special stub that bypasses the whole intent matching and take raw input just like that

2 Likes

+1 @Abir I am struggling with same stuff using form. I want to ask subject for meeting which is one of the slots. The text should be used for subject only when requested_slot=subject… I am not even able to check if requested_slot==subject with rasa 1.0.

Hi @isgaal …in your scenario you have only to fetch one user message. In my scenario I need to fetch 3 user sent messages and save them in a single action file. How do I do this? Because the latest message keeps changing every time. I need to set 3 of these latest messages.

I agree… How to trigger any text “without intent” inside the storie?

if you found solutions please let me know …how to send free text in rasa chatbot…? i am searching solution from many days but did not found yet…