Resolving multiple Alexa slots with Jovo 4


#1

Hello all,

I am attempting to migrate from Jovo 3 to Jovo 4 and so far have been having some difficulty due to the lack of examples (I learned Jovo 3 by working through the tutorials). The part I am currently stuck on is how best to fill in multiple slots using the new component model. Here’s my specific use-case:

I have a parent Intent called ScheduleMeetingIntent, this intent then tries to extract three pieces of information from the user: Date, Time, and Topic. In Jovo 3, I chain states together to fill the slots: I have a ScheduleMeetingIntent handler that has a follow up state of GetDateState which has a follow up state of GetTimeState which has a follow up state of GetTopicState which then redirect to a CreateMeeting stateless intent which calls an external API and creates the meeting.

In Jovo 4 I have tried creating a parent component called ScheduleMeetingComponent that redirects to a DateComponent. However, the GetDateIntent is never triggered - when I prompt the user for the date, the answer keeps going to the DateComponent UNHANDLED method. What am I doing wrong? Also, can I collect the information to fill all 3 slots from the top level schedule meeting component (e.g. go to date and back to schedule meeting, then collect time and go back, etc.) instead of chaining the date, time, and topic components together?

Here are some code snippets to help visualize my questions:
ScheduleMeetingComponent.ts

 @Component({
      components:
        [
          UnbluDateComponent,
        ]
    })
    export class UnbluScheduledMeetingComponent extends BaseComponent {

      START() {
        console.log(`In START`)
        return this.$redirect(UnbluDateComponent);
      }

      UNHANDLED() {
        console.log(`UnbluScheduledMeetingComponent: UNHANDLED`)
        return this.START();
      }

    }

DateComponent.ts

    @Component({
      components: [
        UnbluTimeComponent
      ]
    })
    export class UnbluDateComponent extends BaseComponent {

      START() {
        console.log(`In UnbluDateComponent START`)
        return this.$send({ message: 'What is the date of the meeting?', listen: true});
      }

      @Intents(['GetDateIntent'])
      getDate() {
        const date = this.$entities.date?.value;
        console.log(`Getting date: ${date}`);
        this.$session.data.date = date;

        return this.$redirect(UnbluTimeComponent);
      }

      UNHANDLED() {
        console.log(`UnbluDateComponent: UNHANDLED`)
        return this.START(); 
      }

    }

en.json (for the date intent)

    "GetDateIntent": {
       "phrases": [
         "the date is {date}",
         "{date}"
       ],
       "entities": {
         "date": {
           "type": {
             "alexa": "AMAZON.DATE",
             "dialogflow": "@sys.date"
           }
         }
       }
     },

Jovo 4 slot filling
#2

I would probably use a ScheduleMeetingComponent that uses the $delegate() method to delegate to the subcomponents GetDateComponent, GetTimeComponent and GetTopicComponent.

Here’s an example:

// src/components/ScheduleMeetingComponent.ts

import { Component, BaseComponent } from '@jovotech/framework';

@Component()
class ScheduleMeetingComponent extends BaseComponent {
  START() {
    // ...
    return this.$delegate(GetDateComponent, {
      resolve: {
        success: this.onDate,
        // ...
      }
    });
  }

  onDate(date) {
    // Store date in component data
    this.$component.data.date = date;
   
    // Delegate to next subcomponent
    // ...
  }
}

The GetDateComponent then prompts for the date in START and uses the $resolve method to return the value when it’s retrieved.

// src/components/GetDateComponent.ts

import { Component, BaseComponent } from '@jovotech/framework';

@Component()
class GetDateComponent extends BaseComponent {
  START() {
    // ...
    return this.$send('When would you like to meet?');
  }

  @Intents([ 'DateIntent' ])
  resolveDate() {
    // ...
    return this.$resolve('success', this.$entities.date.resolved);
  }  
}

This could be done with additional subcomponents until all the data is retrieved. A createMeeting handler inside ScheduleMeetingComponent could then make the final confirmation.

I think it’s better to use the $delegate() method here. This way, you don’t lose the existing component in the $state stack.

This is indeed weird behavior. Where are you testing? In the Jovo Debugger? Could you send the $input object for the interaction that goes into the wrong handler?

Yes, you could also add the handlers for each of the slots inside the ScheduleMeetingComponent. This would potentially make the interaction a bit less rigid, too.


#3

Hi @jan ,

Thanks for the response - that was a big help in getting a better understanding of the overall flow of redirect/delegate in Jovo 4. I was able to deploy to Alexa developer and it processed through the Schedule Meeting state machine filling in each slot (Date, Time, Topic) along the way.

However, I am still having a problem triggering the GetDateIntent when using the Jovo debugger (same Jovo webhook works fine with Alexa Developer Test). Here is the $input object that is not being resolved to an Intent:

$input.nlu
{
  "intent":
  {
    "name": "None"
  }
  "entities": {
  }
  "raw": {
    "locale":    "en",
    "utterance":    "tomorrow",
    "languageGuessed":    false,
    "localeIso2":    "en",
    "language":    "English",
    "nluAnswer": {
      "classifications": [
        {
          "intent": "RegisteredUsersIntent",
          "score": 0
        }
        {
          "intent": "YesIntent",
          "score": 0
        }
        {
          "intent": "AllUsersIntent",
          "score": 0
        }
        {
          "intent": "NoIntent",
          "score": 0
        }
        {
          "intent": "GetTimeIntent",
          "score": 0
        }
        {
          "intent": "GetDateIntent",
          "score": 0
        }
        {
          "intent": "AdminUsersIntent",
          "score": 0
        }
        {
          "intent": "ScheduleMeetingIntent",
          "score": 0
        }
        {
          "intent": "UnbluVersionIntent",
          "score": 0
        }
        {
          "intent": "GetTopicIntent",
          "score": 0
        }
      ]
    }
    "classifications": [
      {
        "intent": "RegisteredUsersIntent",
        "score": 0
      }
      {
        "intent": "YesIntent",
        "score": 0
      }
      {
        "intent": "AllUsersIntent",
        "score": 0
      }
      {
        "intent": "NoIntent",
        "score": 0
      }
      {
        "intent": "GetTimeIntent",
        "score": 0
      }
      {
        "intent": "GetDateIntent",
        "score": 0
      }
      {
        "intent": "AdminUsersIntent",
        "score": 0
      }
      {
        "intent": "ScheduleMeetingIntent",
        "score": 0
      }
      {
        "intent": "UnbluVersionIntent",
        "score": 0
      }
      {
        "intent": "GetTopicIntent",
        "score": 0
      }
    ]
    "intent": "None",
    "score": 1,
    "domain": "default",
    "sourceEntities": [
    ]
    "entities": [
    ]
    "answers": [
    ]
    "actions": [
    ]
    "sentiment": {
      "score": 0,
      "numWords": 1,
      "numHits": 0,
      "average": 0,
      "type": "senticon",
      "locale": "en",
      "vote": "neutral"
    }
  }
}

Thanks again for the assistance in getting me on track.

Regards,
Ben

#4

Nice! May I ask what design you went for? Different components for each step? One component for all steps?

Ah yes, thank you for the additional detail. This is an issue where we need to get better at showing how the Debugger works:

The Debugger is not connected to the Alexa platform, it uses its own natural language understanding (NLU) integration to turn raw text into structured meaning. By default, it uses NLP.js, but it can also be customized to any other Jovo NLU integration. You can learn more here: https://www.jovo.tech/docs/debugger#nlu

Your $input data suggests that NLP.js wasn’t able to detect the intent (it says None in the result), which is probably because the entities object for that intent in your Jovo Model only defines types for alexa and dialogflow.

We’re working on improving this, but here are a few workarounds that you can use:

  • Additionally test this the app in the Alexa Developer Console simulator (which uses the Alexa NLU and is closer to the live result than the Debugger’s NLU)
  • For this entity, add an additional type for NLP.js by adding a nlpjs field (I’m not sure NLP.js supports a date type though)
  • Test this portion of the app using buttons instead of text input. You can define an intent and entities that should get sent by the click of a button. Learn more here: https://www.jovo.tech/docs/debugger-config
  • Use a different NLU for the Debugger, for example AWS Lex which is closer to the Alexa NLU. You can learn more here: https://www.jovo.tech/docs/debugger#nlu

#5

Hi @jan,

Thanks for the explanation regarding the Jovo debugger - I’ve started using a combination of the Alexa developer console and the Jovo debugger which works well for me.

Nice! May I ask what design you went for? Different components for each step? One component for all steps?

I ended up creating a top-level parent Schedule Meeting component with individual sub components for getting the date, the time, and the topic of the meeting. As you suggested, I used a delegate to each of these individual components one at a time. On success, there is a method in the Schedule Meeting component that delegates on to the next slot to fill. I can see a potential future need for getting things like the date or the topic for other use-cases of my skill, so I like having these as individual components which can be re-used elsewhere.

One issue that I still did have was once I collected the 3 pieces of information, I wanted to then call my internal API to setup the meeting. I wanted this method, createMeeting(), to be at the Schedule Meeting component level being called after I received the topic of the meeting from the user. I ended up just calling it directly from onTopic, but I wasn’t certain if this is the best approach. Here’s some code snippets to demonstrate:

export class ScheduleMeetingComponent extends BaseComponent {
.
.
.
  onTime(time: any) {
    this.$component.data.time = time;
    return this.$delegate(TopicComponent, {
      resolve: {
        success: this.onTopic
    }
  });

  // onTopic is called after the last slot is filled  
  // and success is returned 
  onTopic(topic: string) {
    this.$component.data.topic = topic;
   return this.onComplete();
  }

  onComplete() {
    const date = this.$component.data.date;
    const time = this.$component.data.time;
    const topic = this.$component.data.topic;
    // Call my internal API HERE
  .
  .
  .
    // Give feedback to the caller
    return this.$send(`Ok, I created a meeting for you on ${date} at ${time} to discuss ${topic}.`);
  }
}

But anyway, thanks again for the advice and help. One of the reasons I use and recommend jovo is the excellent support from the community group and this is just one more example that demonstrates this.

Regards,
Ben


#6

What exactly you’re doing with this second parameter?

return this.$resolve('success', **this.$entities.date.resolved**);

is it used to send back the value to the master component? where did .resolved came from?

Thanks for the detailed example


#7

Hi @vpego,

Yes, this.$entities.date.resolved is being sent back to the master component. Here is what the relevant part of the master component looks like:

START() {
    console.log(`In START`)
    return this.$delegate(DateComponent, {
      resolve: {
        success: this.onDate
      }
    });
  }

  onDate(date: any) {
    // Store date in component data
    this.$component.data.date = date;
    console.log(`Storing date: ${date}`);

    return this.$delegate(TimeComponent, {
      resolve: {
        success: this.onTime
      }
    })
  }

So, my (limited) understanding is that on success, the child component returns the data (the second argument that you indicated above). Here again is the code snippet from the child DateComponent:

@Intents(['GetDateIntent'])
  resolveDate() {
    const date = this.$entities.date.value;
    console.log(`Getting date: ${date}`);
    return this.$resolve('success', this.$entities.date.resolved);
  }

From what I see in the Jovo docs (https://www.jovo.tech/docs/entities#access-entities) , this second argument can be value, resolved, id, or native. The docs say that resolved is, If the entity value was a synonym, the “main” value of the language model will be provided here. If there is no resolved value, this will default to value.

Hope this helps a little. I’m still learning all this as well.

Regards,
Ben