[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[ ] Performance issue
[ ] Feature request
[x ] Documentation issue or request
[ ] Other... Please describe:
In the Alexa SDK v1 it was possible to transfer handling from one intent handler to another by using emitWithState. e.g. in this document (https://www.npmjs.com/package/alexa-sdk) it states:
To "transfer" a request from one state handler to another which is called intent forwarding, this.handler.state needs to be set to the name of the target state. If the target state is "", then this.emit("TargetHandlerName") should be called. For any other states, this.emitWithState("TargetHandlerName") must be called instead.
What is the correct way to handle this with the V2 SDK and the ResponseBuilder approach? The documentation for ResponseBuilder includes no mention of anything that looks like this that I can find, and none of the examples seem to show it either.
// Not required, but suggest a fix/reason for the bug,
// or ideas how to implement the addition or change
// Provide a self-contained, concise snippet of code
// For more complex issues provide a repo with the smallest sample that reproduces the bug
// Including business logic or unrelated code makes diagnosis more difficult
Hi @viking2917 , is there a specific use case for transferring the handle to a different handler? Can't you have the corresponding logic in the 'canHandle', so that a single handler processes the request?
Hi @nikhilym, thanks for helping.
Let's say I have a handler that handles Task X, and another handler that handles Task Y. I only have so much control over how Alexa interprets voice utterances and routes them to intents. So an utterance which, contextually, I am sure is for Task X, sometimes gets sent to Task Y. Rather than incorporating all of Task X's logic into the hander for Task Y, (and vice versa), I'd rather just send it back to Task X directly. Otherwise I'm replicating a whole bunch of code, or building a single MONSTER handler that does everything....
More specifically, in my case, I have a book recommendation skill where the user can say authors, titles, genres, and topics, and response accordingly. I can disambiguate those queries myself with reasonable precision but Alexa will often send things to an unanticipated handler, because the domain of speech and examples is so broad.
But I don't know that this is so specific to my use case - the previous version of this skill had that capability (e.g. emitWithState), I'm just wondering where it went. I'm trying to port my Skill to the new version of the API and that construct appears to have gone missing....
I can of course work around this, but it's ugly and it seems the previous version of the SDK had this capability.
Hi @viking2917 ,
In the v2 SDK, you can just invoke the target function directly. In the v1 SDK, all informations are injected into the handler function context (this). Therefore, v1 SDK provided an easy way to invoke another handler without losing the context information. In v2, informations are passed in through the handler function parameter (HandlerInput). Therefore, you can just import the other function and invoke it directly with the handlerInput.
For example:
````javascript
const fooHandler = {
canHandle(input) {
...
},
handle(input) {
...
}
}
const barHandler = {
canHandle(input) {
...
},
handle(input) {
...
// call fooHandler
return fooHandler.handle(input);
}
}
````
@tianrenz Ha! Thanks! That is far too easy a solution :) (Of course, one might have to mess around with slot types and intent names but I did a quick experiment and it seems to work just fine. Thank you!
That might be worth documenting...
One way i've approached this is to define skill response logic separately from each canHandle() function
const helpResponse = (handlerInput) => {
return handlerInput.responseBuilder
.speak('How can I help you?')
.reprompt('Sorry, I didn\'t get that, how can I help you?')
.getResponse();
}
const welcomeResponse = (handlerInput) => {
if(someCondition) {
return helpResponse(handlerInput);
}
return handlerInput.responseBuilder
.speak('Welcome to my skill, how can i help you today?')
.reprompt('Sorry, I didn\'t get that, how can I help you?')
.getResponse();
}
In fact, with a few helper functions you can compose a skill as a set of functions, and avoid having to write the canHandle keyword ever again.
const intentIs = (intentName) => {
return (handlerInput) => {
return requestTypeIs('IntentRequest')(handlerInput) &&
handlerInput.requestEnvelope.request.intent.name === intentName;
}
}
const requestTypeIs = (requestType) => {
return (handlerInput) => {
return handlerInput.requestEnvelope.request.type === requestType;
}
}
Then you can compose a skill using the builder by using these helper functions and a particular response isn't tied to a specific canHandle :)
const handler = Builder.custom()
.addRequestHandler(requestTypeIs('LaunchRequest'), welcomeResponse)
.addRequestHandler(intentIs('AMAZON.HelpIntent'), helpResponse)
.lambda();
@viking2917 , glad problem solved. We will make a note to add relevant document sometime!
Closing this issue now.
@brendanclement very clean.
Thanks @viking2917 . It's much easier to write clean code when a handler returns a response
I had a conceptual question about this. Is it correct to say that intent forwarding is a type of state handling?
I see we have done away with the usage of stateHandlers and 'emit with state' methods. Also, HandlerInput seems to have replaced the need to extract from 'this.' What purpose does 'this' now serve?
Hi @fluxquantum ,
this just points to the function context. It doesn't carry any additional SDK attribute now.
Regards
@brendanclement
thanks for the response, it was intriguing. I didn't quite understand it all as I am still trying to get my head around the new v2 SDK and the whole canHandle confusion. Would it be at all possible for you to provide a simple complete example using your technique that mirrors one of the existing demos (e.g. HelloWorld) please?
Hi @viking2917 ,
In the v2 SDK, you can just invoke the target function directly. In the v1 SDK, all informations are injected into the handler function context (
this). Therefore, v1 SDK provided an easy way to invoke another handler without losing the context information. In v2, informations are passed in through the handler function parameter (HandlerInput). Therefore, you can just import the other function and invoke it directly with the handlerInput.For example:
const fooHandler = { canHandle(input) { ... }, handle(input) { ... } } const barHandler = { canHandle(input) { ... }, handle(input) { ... // call fooHandler return fooHandler.handle(input); } }
This works... ...except for the LaunchHandler
const LaunchHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'LaunchHandler'
},
handle(handlerInput) {
return HelpIntentHandler(handlerInput) // <--- fail
}
}
in the console
Error handled: HelpIntentHandler is not a function
@32teeth it's hard to say _exactly_ what's happening without seeing your code, but here are some possible issues:
HelpIntentHandler. Unlikely because you've referenced it, but check anyway.HelpIntentHandlerHelpIntentHandlerYou wouldn't get that error from within the LaunchHandler alone, as there's nothing special about that handler or its scope that would cause that specific error.
@32teeth it's hard to say _exactly_ what's happening without seeing your code, but here are some possible issues:
- You don't have a
HelpIntentHandler. Unlikely because you've referenced it, but check anyway.- You've named it something else than
HelpIntentHandler- You're importing your handlers from other files and didn't import
HelpIntentHandlerYou wouldn't get that error from within the
LaunchHandleralone, as there's nothing special about that handler or its scope that would cause that specific error.
LaunchRequestHandler.js
const LaunchRequestHandler = {
canHandle(input) {
const request = input.requestEnvelope.request;
return request.type === 'LaunchRequest';
},
handle(input) {
return HelpIntentHandler(handlerInput);
},
};
module.exports = LaunchRequestHandler;
HelpIntentHandler.js
/*
* Help Handler
*/
const HelpIntentHandler = {
canHandle(input) {
return input.requestEnvelope.request.type === 'IntentRequest'
&& input.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
},
handle(input) {
const speechText = 'You can say hello to me!';
return input.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withSimpleCard('Hello World', speechText)
.getResponse();
},
};
module.exports = HelpIntentHandler;
index.js
/* eslint-disable func-names */
/* eslint-disable no-console */
Alexa = require('ask-sdk');
LaunchRequestHandler = require('./handlers/LaunchRequestHandler');
HelpIntentHandler = require('./handlers/HelpIntentHandler');
ErrorHandler = require('./handlers/ErrorHandler');
/* more code */
let skill = false;
exports.handler = async (event, context) => {
if (!skill) {
/*
* Build Skill
*/
skill = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler
,HelpIntentHandler
)
.addErrorHandlers(ErrorHandler)
.create()
}
const response = await skill.invoke(event, context);
return response;
}
My apologies
I have found my delta
Seems the problem was between the keyboard and the screen (LOL)
return HelpIntentHandler(handlerInput);
shoule be
return HelpIntentHandler.handle(handlerInput);
@tianrenz the solution you provided (https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/issues/408#issuecomment-397018053) only appears to work with non-dialog intents. When handler A returns the result of handler B (and in the case where handler B delegates) Alexa ends the session with this error: Only updated intent of the same type can be sent with a \"Dialog.Delegate\" directive.
Specifically, the response from Alexa in the case described above looks like this:
handlerInput.requestEnvelope.request.type is 'SessionEndedRequest'
handlerInput.requestEnvelope.request.error.message is Only updated intent of the same type can be sent with a \"Dialog.Delegate\" directive
And since the session has now ended, the dialog can not continue. That being said, the approach you described works fine if handler B is not delegating. Unfortunately, we make extensive use of the dialog system.
Can you recommend a workaround so that we can forward to a dialog-utilizing intent?
P.S. Thank you for your excellent SDK. It's very clear how much thought you and your team put into the SDK based off the well-constructed types that you made available through the d.ts files. In particular I enjoyed your approach of utilizing string literal types for the type property so that you can do nominal typing and still take advantage of union types. Well done! Much gratitude. And hopefully you can recommend a solution to the error above. :)
Hi @dgreene1 ,
Thanks for your support in SDK! 馃憤
Regarding your use case above, could you provide information to questions below:
If you can attach some code snippet of handler A and B, that would help a lot as well!
Regards
Great questions, and I apologize for not producing a large code snippet first. And thank you for the speedy response.
"does handler A also handle a intent request that is dialog enabled?"
No. Handler A has slots, but it does not require confirmation nor delegate. So I believe that makes it not a dialog. Sorry-- the terms are quite strange to me.
"when handler B handles intent, how does it return delegate directive?"
I'll show a code snippet below that demonstrates it how A calls B which delegates.
"When forwarding from A to B, B.handle method should modify the incoming request."
How might our code accomplish that? Does that mean that it's possible to mutate the request? I assumed that the request was immutable or that the server-side was comparing the response vs the request it had stored. Because that would explain the error message (Only updated intent of the same type can be sent with a \"Dialog.Delegate\" directive). In other words, I made an assumption based on the error text that you can't/shouldn't try to fake out Alexa.
Here's a redacted form of the code:
import * as Alexa from 'ask-sdk';
export const a : Alexa.RequestHandler = {
canHandle() {
return true; // For simplicity
},
async handle(handlerInput) {
return b.handle(handlerInput);
},
};
export const b : Alexa.RequestHandler = {
canHandle() {
return true; // For simplicity
},
async handle(handlerInput) {
return handlerInput.responseBuilder.addDelegateDirective().getResponse();
},
};
@dgreene1 , sorry I made a typo above. it should be
When forwarding from A to B, B.handle method should NOT modify the incoming request.
For the event to be "dialog enabled", I meant if the intent or any of it's slots require elicitation or confirmation? Your handler B should only return a delegate directive in response to a request that requires above.
Regards
@tianrenz I am not modifying the input. The issue occurs when handlerB delegates and handlerA does not have any slots that require delegation.
A similar issue happens if handlerB calls addElicitSlotDirective for a slot that handlerA does not have.
A potential (but onerous) workaround is to make sure that handlerA has every slot that handlerB has. This is burdensome and error prone because our skill uses handlerA as a passthrough for many handlers. So if there are 10 handlers that have 2 slots each, then the passthrough handler (handlerA) must have 20 slots.
I think it might be helpful if I provided a user story:
As a parent using the "find children" skill, if I ask "where is my child" it should respond (with handlerB). If I have more than one child, I should first be asked which child I am asking about (that's what handlerA does).
Now that's a generalized version our use case. But the point is that handlerA asks an additional question conditionally. It could be thought of as a non-void requestInterceptor. Yes, we could add that conditional question into each of other 10 handlers, but that's not very DRY.
Does properly explain the limitation that my team and I (believe that we) have found?
Hi @dgreene1 ,
Sorry for the delay in response. I think this is a common misunderstanding of "intent forwarding".
When we "forward" an intent from handler A to handler B. SDK doesn't really change the content of the original intent your skill receives. Handler B needs to return a response that's suitable for handling the original request.
For example, If handler A receives an intent that has two slots and requires elicitation, then when handler A "forward" the intent to handler B, handler B needs to return a proper response that contains ElicitSlot directive or delegate directive. However, if handler A receives an normal intent that doesn't utilize the dialog interactive, then if A "forwards" the request to handle B which still returns any dialog directives, it will result in error.
For the user story above, I recommend the following setup:
"Where's my child" => FindMyChildrenIntent with an optional slot (not required means no dialog).
"{child name}" => ChildSelectionIntent used to specify which child is user asking for.
When handler A receives the intent:
ChildSelectionIntent in which case can be handled by another handler.Regards
Is there a full example somewhere of this working?
Most helpful comment
One way i've approached this is to define skill response logic separately from each canHandle() function
In fact, with a few helper functions you can compose a skill as a set of functions, and avoid having to write the
canHandlekeyword ever again.Then you can compose a skill using the builder by using these helper functions and a particular response isn't tied to a specific
canHandle:)