11 How to use webhook for slot filling in Dialogflow ES

Dialogflow ES Quickstart Templates Part 2

While the slot filling feature in Dialogflow ES makes for a very good demo, in practice it is really hard to use.

One option is to use webhooks to assist your slot filling. This will give you the best of both the worlds – you allow Dialogflow ES to extract maximum possible relevant information from the user’s first sentence. At the same time, you will exit the slot filling immediately after the first sentence by using follow up events. This helps you avoid the slot filling loop and gives you more fine grained control of your conversation flow. The downside is that this requires a lot more effort on your part.

Note: Dialogflow CX can do all this natively, and without even using a webhook. That is why I think the implementation of slot filling in Dialogflow CX is far superior to the one in ES. 

Let us see how we can convert the flight booking slot filling chatbot to use webhooks for slot filling. The basic idea is quite simple: as soon as the user utters the first sentence

  • we will check all the slots the user has input in the first utterance
  • we will select the next slot based on a pre-existing slot ordering
  • we will trigger a custom event (also called followup event) to exit the slot filling loop
  • as we keep collecting the remaining slots, the webhook code will check which slot has not yet been filled and call followup events for that slot
  • once all slots have been filled, we will trigger a custom event called completed

This article actually covers three concepts simultaneously (it is a pretty complex bot):

  • How to use webhooks for slot filling
  • How to set a parameter value in an output context from your webhook code
  • How to trigger a follow up event (custom event) from your webhook code

First add a session-vars output context to the book.a.flight intent and also select the “Enable webhook for slot filling” toggle.

The webhook code will have a single method called check_slots

@app.route('/webhook', methods=['POST'])
def hello_webhook():
    req = request.get_json(silent=True, force=True)
    with open('data.json', 'w', encoding='utf-8') as f:
        json.dump(req, f, ensure_ascii=False, indent=4)
    result = check_slots(req)
    return result

The check_slots method does all the heavy lifting

def check_slots(req):
    try:
        ordered_slots = ['departuredate', 'returndate', 'flightclass', 'numpassengers', 'fromcity', 'tocity']
        query_result = req.get('queryResult')
        params = query_result.get('parameters')
        context_prefix = get_context_prefix(query_result)
        session_vars = get_session_vars(query_result)
        params_from_session_vars = get_params_from_session_vars(session_vars)
        filled_params = get_filled_params(params, params_from_session_vars)
        first_unfilled_slot = get_first_unfilled_slot(filled_params, ordered_slots)
        result = {
            "outputContexts": [
                {
                    "name": f'{context_prefix}/session-vars',
                    "lifespanCount": 50,
                    "parameters": params
                }
            ],
            "followupEventInput": {
                "name": first_unfilled_slot,
                "languageCode": "en"
            }
        }
        with open('result.json', 'w', encoding='utf-8') as f:
            json.dump(result, f, ensure_ascii=False, indent=4)
        return result
    except Exception as e:
        error_str = str(e)
        return generic_error_message

Notice the following information being returned in the response of the check_slots method

In the outputContexts JSON object array, we send some parameters back to Dialogflow ES inside the session-vars context. This is what I mean when I say “set a parameter value in an output context from your webhook code”

Then we get the first unfilled slot and use it as the follow up event. As you will see later, this in turn will fire a specific intent in your agent which has a custom event that matches the name of the slot. This is what I mean when I say “trigger a follow up event (custom event) from your webhook code”

The get_context_prefix method gets the prefix string which is sent to the webhook for all output context objects as shown in the image.

This is the code for the get_context_prefix method

def get_context_prefix(query_result):
    output_contexts = query_result.get('outputContexts')
    first_output_context = output_contexts[0]
    context_prefix = (str(first_output_context.get('name')).rsplit('/', 1))[0]
    return context_prefix

Then we get the session-vars object. In the first webhook call, it will be empty. This is because the session-vars is “populated” only after the webhook call returns.

def get_session_vars(query_result):
    session_vars = None
    output_contexts = query_result.get('outputContexts')
    session_vars_iter = [context for context in output_contexts if
                         str(context.get('name')).endswith('/session-vars')]
    if len(session_vars_iter) > 0:
        session_vars = session_vars_iter[0]
    return session_vars

Then we get the list of parameters from the session-vars object. We need to “repopulate” this into the output context.

def get_params_from_session_vars(session_vars):
    params_from_session_vars = []
    if session_vars:
        for key, value in session_vars.get('parameters').items():
            if str(value).strip() != '' and not str(key).endswith('.original'):
                params_from_session_vars.append(key)
    return params_from_session_vars

Then we find the list of parameters which have already been populated. We need to combine the parameters which were extracted in the current step and the parameters which were extracted in the previous steps.

def get_filled_params(params, params_from_session_vars):
    filled_params = []
    for key, value in params.items():
        if str(value).strip() != '' and not str(key).endswith('.original'):
            if key not in params_from_session_vars:
                filled_params.append(str(key).strip())
    filled_params.extend(params_from_session_vars)
    return filled_params

Once we do all these steps, we are ready to calculate the first unfilled slot

def get_first_unfilled_slot(filled_params, ordered_slots):
    first_unfilled_slot = 'completed'
    for slot in ordered_slots:
        if slot not in filled_params:
            first_unfilled_slot = slot
            break
    return first_unfilled_slot

For each slot, we will create two intents. The first intent will have the name followupevent.<slotname> and it will have the custom event and one response. Do not add training phrases into this intent. In addition we will set an output context called await_<slotname> for this intent. This intent will not call the webhook.

Then we will have another intent called provides.<slotname>. It will use await_<slotname> as the input context and here we will have training phrases corresponding to the slot data type. This intent will call the webhook.

Example intent definitions for departure date slot

Here is the example intent definition for the tocity slot

You can do the same for the other slots and complete the bot

Here is a demo of the bot in action

And another:


About this website

I created this website to provide training and tools for non-programmers who are building Dialogflow chatbots.

I have now changed my focus to Vertex AI Search, which I think is a natural evolution from chatbots.

Note

BotFlo was previously called MiningBusinessData. That is why you see that watermark in many of my previous videos.

Leave a Reply