Keine Ergebnisse gefunden
We’ve all been in situations where we need to update code that we haven’t touched in a long time. Or we have to troubleshoot code that someone else wrote. Simpler and more streamlined code is typically much easier to maintain, even more so when the same code is not repeated in multiple places. The interceptors available in the Alexa Skills Kit (ASK) Software Development Kit (SDK) are a great tool for simplifying and streamlining your skill code.
The ASK SDK includes both request and response interceptors. These interceptors are executed every time your skill code is invoked. That makes them ideal for completing tasks that you want to be performed every time your skill is invoked. Request interceptors are executed prior to the main handler being selected and executed, while response interceptors are executed after the selected handler completes its work. You can see this flow in this architecture diagram. Every request interceptor is executed in order, as are the request interceptors, while only one of the handlers is executed for each invocation.
A common task performed in a request interceptor is logging the request payload. By doing it there, you don’t need to repeat that code in every handler. A request inceptor is also a good place to load persistent attributes. A good use of a response interceptor would be to save the persistent attributes.
In this blog, I’ll use an interceptor to fetch and cache details about in-skill products for a monetized Alexa skill. This will include not only the product details, but also if the customer is eligible to purchase the product, and if the customer has already purchased them. This way, no matter what intent is invoked, my code will have access to this data and I don’t have to worry about including that code in every possible path. For this blog, I am using Python for the sample code; however, interceptors and the techniques used also apply to skills that use the ASK SDK for Node.js and the ASK SDK for Java.
More and more skills are including in-skill purchasing (ISP) in the functionality they offer. As with any application, as you add more functionality, you must weigh various tradeoffs relating to user experience, performance, resources and more. One of those decisions is how to manage calling the Alexa Monetization Service (AMS) for details about your product catalog and your customers’ purchase statuses.
One approach is to call the AMS every time you need either product or purchase information. This will be fine if you only need to call the service rarely, say once per session. Any more than that, since the information is unlikely to change during a session, that approach will result in more calls than necessary. This will introduce additional latency, albeit only a small amount for each call. This additional latency will have two impacts: first and foremost, the customer will wait longer for Alexa’s response. Again, this is likely to be minimal, but even small amounts of latency can add up. The other impact is to the AWS Lambda function’s run duration.
You can build and host most skills for free with AWS Lambda, which is free for the first one million calls per month through the AWS Free Tier. You can also apply for AWS promotional credits if you incur AWS charges related to your skill. Regardless of what charges are incurred, the compute time portion of the Lambda function pricing is based on GB-seconds (also referred to as Duration). Therefore, the more latency the function experiences (i.e. the longer the function runs), the more billable duration is incurred. Even though you might not incur charges in your skill, reducing your function run time will lead to smaller Lambda billable duration.
For most skills, the details about both a skill’s product and that customer’s purchase status will not change over the course of any given skill session. As a result, a single call to the AMS will suffice to obtain the product details, what the customer has purchased, and what is available for the customer to purchase. This information can be stored in session attributes, and then it will be passed with each request without having to query AMS. When the session ends, the data will be discarded. When a new session starts, the process will start over with the call to AMS to get the data.
As with any caching solution, it is important to invalidate data when it is no longer current/correct. This is true with this solution as well, and the point at which the data logically changes is when the customer makes a purchase. From the customer point of view, the in-skill product is part of the same skill session that precedes and follows the purchase flow. These are in fact, different sessions. You may recall that you need to save any pertinent context information as persistent attributes prior to sending the Connections.SendRequest directive, and the reconstitute the data/attributes after control is returned to your skill. When the Connections.Response event is received by your skill, this starts a new session, and this is represented in the request payload.
Ideally the caching should occur once during the session, and the best time is at the start of the session so that the data is available for every request in the session. Your skill code should examine the request payload and if it finds the new session flag, then it’s a new session and it should fetch the data from AMS.
Rather than attempt to identify all the possible Intent Handlers that could be triggered when a session is initiated, the approach described in this blog uses a RequestInteceptor to watch for a new session. When a request initiates a new session, the interceptor calls AMS and caches the data in a session attribute. Unlike the handle function of handler which is only called if the canHandle returns true, the process method of every RequestInterceptor is called for every request.
This approach also works for the cases where a customer buys a product. When returning from the purchase flow (initiated by the Connections.SendRequest directive and results in a Connections.Response event being sent to your skill), the Connections.Response event request payload has the session.new attrbitue set to true. Since a customer might have purchased a new product, this is definitely an appropriate time to call AMS and get fresh data.
Before we get to adding the main interceptor to our python code, we need to setup a few things. First, we need to import the DefaultSerializer from the ASK SDK for Python. This will serve a key role in interceptor logic.
from ask_sdk_core.serialize import DefaultSerializer
In addition, if you haven’t already, you’ll need to add the following imports as well:
from ask_sdk_core.dispatch_components import AbstractRequestInterceptor
from ask_sdk_model.services.monetization import EntitledState
Next, we’ll create a few helper functions. The first helper function checks the request for the new session flag.
def is_new_session(request_envelope):
"""Checks to see if the request is the first of a session"""
# type: (RequestEnvelope) -> bool
if request_envelope.session.new == True:
return True
return False
The next one filters the list of products to only include the products the customer has already purchased or is ‘entitled’ to. Depending on your use case, you might leave the list unfiltered or break the list up into separate lists based on purchase status or product type.
def get_all_entitled_products(in_skill_product_list):
"""Get list of in-skill products in ENTITLED state."""
# type: (List[InSkillProduct]) -> List[InSkillProduct]
entitled_product_list = [
l for l in in_skill_product_list if (
l.entitled == EntitledState.ENTITLED)]
return entitled_product_list
Now that we have those helper functions in place, let’s add the main interceptor. We’ll name it “load_isp_data_interceptor”. (I like to end my interceptors with “interceptor” so I can quickly distinguish them; you can use whatever naming convention you like.)
Unlike Intent Handlers, Request Inteceptors have only one method “process”, since the interceptor processes every request. Here is the load_isp_data_interceptor class:
class load_isp_data_interceptor(AbstractRequestInterceptor):
"""queries monetization service"""
def process(self, handler_input):
# type: (HandlerInput) -> None
print("Starting Entitled Product Check")
Here is where the helper function is called to check if the session is new or not.
if is_new_session(handler_input.request_envelope):
# new session, check to see what products are already owned.
try:
logger.info("new session, so see what is entitled")
This code looks up the locale in the request. ISP is currently only available in the en-US locale, but looking it up will help to future proof your code.
locale = handler_input.request_envelope.request.locale
Now the interceptor creates a client for the AMS and the gets all the skill product details for the given locale. This call gets the product details and the purchase status of each product.
ms = handler_input.service_client_factory.get_monetization_service()
result = ms.get_in_skill_products(locale)
After we have the results, we filter the result set to only include the products which the customer has purchased. As noted earlier, your use case may have different needs, so be sure to adjust this as needed.
entitled_products = get_all_entitled_products(result.in_skill_products)
If there are any products left after the filtering is complete, then we will store the result set in a session attribute named “entitledProducts.”
if entitled_products:
session_attributes = handler_input.attributes_manager.session_attributes
session_attributes["entitledProducts"] = entitled_products
except Exception as error:
logger.info("Error calling InSkillProducts API: {}".format(error))
raise error
else:
logger.info("not a new session, deserialize if needed")
If the session is not new, there is a little bit of work we need to do so that we can write code that interacts with the entitledProducts session attribute in a consistent manner. Initially when we save the entitled product list, it is stored as InSkillProduct objects. When it is serialized / deserialized during subsequent requests, the data is deserialized as standard python objects, which means we can’t interact with them in the same way. If there is at least one entitled product, we’ll fix this by using the DefaultSerializer to deserialize the list as the original InSkillProduct object type.
session_attributes = handler_input.attributes_manager.session_attributes
entitled_products = session_attributes.get("entitledProducts", None)
if entitled_products:
d = DefaultSerializer()
entitled_products = json.dumps(entitled_products)
session_attributes["entitledProducts"] = d.deserialize(
entitled_products,
'list[ask_sdk_model.services.monetization.in_skill_product.InSkillProduct]')
The last step to enabling this interceptor is to add it to the SkillBuilder object. In the below code, “sb” is a StandardSkillBuilder, so if you named your SkillBuilder object differently, adjust accordingly. Check out the Skill Builder Objects – To Customize or Not To Customize blog for more information on the difference between standard and custom Skill Builder objects.
sb.add_global_request_interceptor(load_isp_data_interceptor())
That’s it! You now have an interceptor in your python skill that caches ISP data. If you’d rather use Node.js to do the same thing, keep reading. The Node.js code is at the end of this blog.
We’re excited to see what you build with ISP! Tweet me @franklinlobb and I’d be happy to check it out!
Helper Function:
function getAllEntitledProducts(inSkillProductList) {
const entitledProductList = inSkillProductList.filter(record => record.entitled === 'ENTITLED');
console.log(`Currently entitled products: ${JSON.stringify(entitledProductList)}`);
return entitledProductList;
}
Interceptor Code:
const loadISPDataInterceptor = {
async process(handlerInput) {
if (handlerInput.requestEnvelope.session.new === true) {
// new session, check to see what products are already owned.
try {
const locale = handlerInput.requestEnvelope.request.locale;
const ms = handlerInput.serviceClientFactory.getMonetizationServiceClient();
const result = await ms.getInSkillProducts(locale);
const entitledProducts = getAllEntitledProducts(result.inSkillProducts);
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
sessionAttributes.entitledProducts = entitledProducts;
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
} catch (error) {
console.log(`Error calling InSkillProducts API: ${error}`);
}
}
},
};
Add Interceptor to Skill Builder:
.addRequestInteceptor(loadISPDataInterceptor)