05.01.2017       Выпуск 159 (02.01.2017 - 08.01.2017)       Статьи

Flask-Ask — используем мощь Alexa на Python

Читать>>



Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

First, speech is send to the Alexa Voice Services (AVS), where the appropriate Alexa Skill requested is selected. If an intent (action) was provided during speech, this intent will be sent towards the selected Alexa Skill implementation, together with potential provided variables. If no intent was provided, e.g. opening the application, the basic Launch intent will be called upon.

Next, based on the provided intent, a certain method will be called upon in the deployed Flask-Ask service. This method will, for example, do a certain calculation based upon the provided variables and will formulate an appropriate response. This response is then send back to the Amazon Echo device, via the AVS. As seen on the figure above, two different responses are available. Logically, a textual response can be provided, which will be executed by the Echo device in the form of speech. However, a card can also be made, providing extra visual information to the users via the Alexa app.

The first application — IGN Reviews

Now that we have established how the Amazon Echo devices operate, we can start building our first application! In order to make sure the developed Alexa skill is available to our Echo device, both the skill and AVS need to be implemented, working together to form the desired application. First, the back-end logic will be implemented. Afterwards, the AVS will be set-up to deploy our skill onto Echo device.

The Alexa Skill —Providing Intelligence

To start implementation of this first application, we will have to navigate to the alexa.py file in PythonAnywhere. To do this, click the Files tab and next, click on mysite/ under Directories. Here, click on alexa.pyto open up our webserver implementation. You will see, that as an example, a very simple “Hello World” app is provided. As we want to start from scratch, the provided code can be wiped.

First of all, we will import the needed libraries used in this first application. Flask and Flask-Ask are used to set up the Alexa skill, datetime will be used to sort reviews by time, requests will be used to access external services, urllib will be used to encode url strings and re will be used for filtering needed data.

from flask import Flask, render_template
from flask_ask import Ask, statement, question, session
from datetime import datetime
import requests
import urllib
import re




Next, the Alexa skill needs to be initialized and a first welcome message should be provided. The initialization of this endpoint is set under the /alexa subdomain. The welcome message is provided by implementing a function underneath the basic launch intent.

app = Flask(__name__)
ask = Ask(app, "/alexa")
@ask.launch
def new_ask():
welcome = render_template('welcome')
reprompt = render_template('reprompt')
return question(welcome) \
.reprompt(reprompt)




Here, the text used to welcome users (welcome) and to rephrase if no input is given (reprompt) is loaded from an external file, called templates.yaml. This file should be created on PythonAnywhere, in the same folder as the alexa.py file. In this file, textual input is given, such that code and text can be separated. For now, we just define the welcome and reprompt text.

welcome: Welcome, to one of my homebrew applications. What would you like me to do?
reprompt: I didn't quite get that. Please state again what you would like me to do.

Now that a first welcome message is provided, we can put some actual intelligence in the application. First, we will implement several methods to get and process data from the IGN website.

def processShowName(show):
showSynonymDict = {}
showSynonymDict['Marvels Agents of Shield'] = "Marvel's Agents of S.H.I.E.L.D."
showSynonymDict['Legends of Tomorrow'] = "DC's Legends of Tomorrow"
if show in showSynonymDict:
return showSynonymDict[show]
else:
return show






Later, when defining the Alexa Voice Service, we will set up multiple possibilities of shows that we can ask the reviews for. The processShowName method allows for this pronounced show as a string to be transformed into the actual name of the show. This will enable the correct results to be found by the getReviews method. In this method, an other platform called import.io is used. This allows for an easy way to build APIs from structured websites.

def getReviews(show):
url = ("http://www.ign.com/search?page=0&count=10&filter=articles&type=article&q=review " + show).replace(" ", "%20")
url = urllib.quote(url, safe='')
urlRest = "https://api.import.io/store/connector/87e7f75c-3d4c-45cb-93e9-305a52237e63/_query?input=webpage/url:" + url + "&&_apikey=e581e9228fc34f139c3031d520f27af635f6dc576c932b84a8fd6cce4cc4b6a0a8499e8fcc53a7e308bf321c7a6e22998ec481eb1ab47742b062d01d62e174fc95343090d71c6dc63a4255886bf546e1"
data = requests.get(urlRest).json()
return data




By using the getReviews method, an JSON object is returned, filled with reviews of the requested show. To obtain this, first, the search results page url is formed. Next, it is encoded and added into the self-made import.io API. Finally, this API will return the JSON object, containing a multitude of reviews of the requested show, together with some extra useful information.

def processDates(data):
dateDict = {}
for i in range(0, len(data["results"])):
regex = re.compile('(?<=http:\/\/www.ign.com\/articles\/)(.*)(?=\/.*)', re.IGNORECASE)
date_str = regex.findall(data["results"][i]["searchitem_link_2"])[0].split("/")
dateDict[i] = datetime(int(date_str[0]), int(date_str[1]), int(date_str[2]))
return dateDict





As we are requesting the latest review for that show, we can sort all reviews in the JSON object by date, returning a dictionary with their position in the JSON array as key and their respective date-stamp as value. This date-stamp is extracted using a regex on a certain part of the acquired data.

def getLatestReview(show):
data = getReviews(show)
dateDict = processDates(data)
i_latest = 0
for i in dateDict:
if dateDict[i] > dateDict[i_latest]:
i_latest = i
latest = data["results"][i_latest]
return latest







Lastly, we put it all together using the getLatestReview method. This will receive a certain show as a string, get the JSON data with reviews from that show, get the dictionary containing the date-stamps for those reviews and return the JSON data for the latest review.

Now, we can put this logic to actual use in the voice application. In order to provide the intended results, we need to define new speech templates in the templates.yaml file.

latest_review: The latest episode of {{ show }}, called, {{ episode }}, got a score of {{ score }} on IGN.
reprompt_show: I didn't quite understand that. What show did you want the review from?

To implement these speech templates, we create a response to the intent ReviewLatestIntent. This intent will be called upon when the AVS gets the appropriate question to give the latest review on a certain show.

@ask.intent('ReviewLatestIntent')
def launchReview(show):
if (show is None):
reprompt_show = render_template("reprompt_show")
return question(reprompt_show)
else:
filShow = processShowName(show)
latest = getLatestReview(filShow)
review_title = latest["searchitem_link_2/_text"]
episode_title = review_title.split(':')[1].replace('"','')
if (" Review" in episode_title):
episode_title = episode_title.replace(" Review", "")
image_url = latest["searchitem_image"].replace("_160w", "_480w").replace("http", "https")
score = str(latest["reviewscore_number"])
description = latest["ignreview_description"]
latestReview_msg = render_template('latest_review', show=show, episode=episode_title, score=score)
return statement(latestReview_msg) \
.standard_card(title=review_title + " - " + score,
text=description,
small_image_url=image_url,
large_image_url=image_url)



















Note that the launchReviewmethod has a variable called show. This will be provided by the Alexa Voice Service, and will either be a show from a range of possibilities or a NoneType object. Therefore, we first need to check whether the provided show is valid or not. If the show is not valid, we again ask for voice input of this show. If it is valid, we convert it to its proper name, get the latest review, and extract some data from the JSON object for clarity. Using this data, we then fill in the gaps in the latest_review speech template and return this as a voice response.

However, we also return a visual response, in the form of a standard card. This will allow the review to be shown on the Alexa app, accompanied with the appropriate picture and text. As for the simplicity of the application, a short description is given as text for the card. I looked for the possibility of adding a link to the review page, so it can be navigated to quickly. Having found another person asking this question, Amazon has stated the following:

The Alexa Skill Kit does not support clickable links. Including a URL text string in a card is not recommended since that may present a poor and confusing user experience.

Reading this statement, it does not seem like they even want to add url functionality into a card. However, using import.io, it would be possible to quickly get the full review in text and display this on the card, removing the need for the url.

The card for the latest review of The Flash, as shown in the Alexa web application

To make sure the Flask-Ask application is runnable, add the following lines of code to the alexa.py file. Finally, go to the Web tab in PythonAnywhere and reload the set up webserver.

if __name__ == '__main__':
app.run(debug=True)

Great! We have just deployed our first Alexa Skill! However, we have not yet made the link between the Echo device and our application. To do this, we have to create the communication part of the Alexa Skill.

The Alexa Skill — Providing Communication

First, you have to login with your Amazon credentials into their developer console. Having done this, navigate to the Alexa tab and select the Alexa Skills Kit. Click on Add a New Skill and we can start filling in the details.

As for Skill Information, we provide IGN Reviews as name for the application and reviews as invocation name. The choice of invocation name is really important, as this will be used to call our service with.

Next, the Interaction Model needs to be provided. First, we will create a list of possible shows, by adding a new custom slot type. For this slot, the type will be LIST_OF_SHOWSand the values are listed as following:

Arrow
Legends of Tomorrow
The Flash
Supergirl
Marvels Agents of Shield
Westworld




Now, the intent schema needs to be filled in. As we have only defined one intent into our application, we will only call upon this single intent. Notice that this intent contains a slots list. In the Alexa skill, this corresponds to the acquired input variables for our launchReview method.

{
"intents": [
{
"intent": "ReviewLatestIntent",
"slots": [{
"name": "show",
"type": "LIST_OF_SHOWS"
}]
}
]
}









Lastly, we define sample utterances. These are example phrases that will correspond to the user interaction with the skill. Even though I have only defined four different possibilities, the Alexa skill will be able to cope with similar speech input.

ReviewLatestIntent give me the latest review on {show}
ReviewLatestIntent give me the latest {show} review
ReviewLatestIntent what the latest {show} review is
ReviewLatestIntent how the last {show} episode scored


To be able to communicate with our Flask-Ask application, we need to set up the Configuration correctly. As a service endpoint type, select HTTPS, your geographical preference and paste the url to your Flask-Ask service here. (https://*yourUserName*.pythonanywhere.com/alexa)

As we are not using authenticated third party services, we can ignore the Account Linking and click Next. Lastly, to setup the SSL Certificate, select the second option and Save the application.

Congratulations! The homebrew Alexa skill is now fully deployed and operational! You can now test it out with your favorite Echo device or use the Echosim.io service to test it out directly on your computer.

The second application —“I’m going on a trip”

To prove that two Alexa skills can be deployed upon a single Flask-Ask service and to show context awareness possibilities in app development, we are going to create a second application called “I’m going on a trip”. As proof of concept, the application will provide us with a certain European city. Next, we can ask follow up questions about that city, like the weather forecast. An example scenario of how an interaction could go, is provided below.

  • Alexa] They say London is beautiful this time of the year.
  • User] What’s the weather like over there?
  • Alexa] In London, the weather is currently being described as cloudy, with a temperature of 2 degrees Celsius.
  • User] Give me another city.
  • Alexa] A great place to visit might be Venice.
  • User] How is the weather?
  • Alexa] In Venice, the weather is currently being described as sunny, with a temperature of 11 degrees Celsius.

Following this example scenario, we will start the implementation of our second application.

The Alexa Skill — Providing Intelligence

First of all, we are going to define some new speech responses in the templates.yaml file.

trip_open: Hi there! Are you ready to go on a trip with me?
trip_again: Are you still here? Tell me when you're ready!
trip_start: Okay great!
trip_city_1: They say {{ city }} is beautiful this time of the year.
trip_city_2: A great place to visit might be {{ city }}.
trip_city_3: Have you ever been to {{ city }}? It's lovely!
trip_city_propose_good: Oh yeah! I haven't been to {{ city }} yet. Great idea.
trip_city_propose_bad: Hmm. I'm not really feeling like going there. Do you have another proposal?
trip_weather: In {{ city }}, the weather is currently being described as cloudy, with a temperature of {{ temp }} degrees Celsius.
trip_go: Great! I've already booked us a flight for tomorrow! Let's start packing!
trip_nogo: But I really wanted to go on a trip with you.

Next, we can begin the implementation of the application. To do this, we open up the alexa.py file, where our Alexa skill is implemented. Here, we define an intent called TripWelcomeEvent, which will be called upon when asking the application to go on a trip. The implementation of this intent is fairly straightforward.

@ask.intent('TripWelcomeIntent')
def trip_welcome():
welcome = render_template('trip_open')
reprompt = render_template('trip_again')
return question(welcome) \
.reprompt(reprompt)




When the user is being asked if he or she is ready, a positive and negative response can be provided. Instead of making our own intents to handle this, we can use standard Amazon intents that have already been extensively tested. First, we will implement the trip_nogotemplate. This should be called upon when the user isn’t ‘ready’ or when the application is cancelled or stopped. As this comprises multiple intents for the same method, an implementation is provided as follows.

@ask.intent('AMAZON.CancelIntent')
@ask.intent('AMAZON.StopIntent')
@ask.intent('AMAZON.NoIntent')
def trip_nogo():
quit = render_template('trip_nogo')
return statement(quit)




Before adding the YesIntent, we will first have to implement the basic logic of the application. As the purpose of the application is to suggest a trip to a certain European city, we want a list of cities from which we can select. This is provided by the getCityList method. For simplicity, we are manually going to put together this list of cities. However, an external service or database could also be used.

def getCityList():
list = ['Paris', 'London', 'Rome', 'Madrid', 'Prague', 'Venice', 'Florence', 'Dublin', 'Kopenhagen']
return list

We also need a method that can return a randomly selected element from the provided list. To use the following method, we need to import the random library.

import random
def getRandomElement(list):
return random.choice(list)

To add some randomness to the provided speech outputs, we also implement a getRandomCityText method.

def getRandomCityText(city):
texts = ['trip_city_1', 'trip_city_2', 'trip_city_3']
choice = getRandomElement(texts)
cityText = render_template(choice, city=city)
return cityText



Now, we are going to summarize the application logic. To do this, a form of memory storage will be set up, which will be available only during the duration of the session. This implies that, if the application is cancelled, stopped or has ended, the stored variables are removed. To implement the standard YesIntent from Amazon, first, select a random city from the provided list of cities. Next, in our session attributes, we make a currentCity and promptedCities variable. This last variable will contain a list with cities already proposed during this session. Lastly, we return speech as a question. This is very important, as a question response does not close the current session, where a statement closes it, losing the saved variables.

@ask.intent('AMAZON.YesIntent')
def trip_start():
start = render_template('trip_start')
cityList = getCityList()
city = getRandomElement(cityList)
promptedCities = [city]
session.attributes['promptedCities'] = promptedCities
session.attributes['currentCity'] = city
cityPhrase = getRandomCityText(city)
return question(start + cityPhrase)








Next, we will implement the TripNextCityIntent, which will be called upon when the user wants another suggestion. The main difference here, is that a check is applied to make sure that a certain city is not prompted twice during a session.

@ask.intent('TripNextCityIntent')
def trip_nextCity():
cityList = getCityList()
city = getRandomElement(cityList)
promptedCities = session.attributes['promptedCities']
while city in promptedCities:
# This can go into a loop if all cities have been prompted
city = getRandomElement(cityList)
promptedCities.append(city)
session.attributes['promptedCities'] = promptedCities
session.attributes['currentCity'] = city
cityPhrase = getRandomCityText(city)
return question(cityPhrase)











In order to let the user have a certain degree of freedom, we will allow the proposal of a city to go to. This will be implemented by the TripProposeIntent, where a city will be provided to the method.

@ask.intent('TripProposeIntent')
def trip_propose(city):
if (city is None):
response = render_template('trip_city_propose_bad')
return question(response)
else:
response = render_template('trip_city_propose_good', city=city)
promptedCities = session.attributes['promptedCities']
promptedCities.append(city)
session.attributes['promptedCities'] = promptedCities
session.attributes['currentCity'] = city
return question(response)










Now that we can ask for suggestions and input our own, it is time to make a follow up question. For simplicity of this application, we are only going to make a single one, namely asking about the weather. For the data, we will use an API provided by OpenWeatherMap, which allows up to search for weather data of a city based on a string.

@ask.intent('TripWeatherIntent')
def trip_weather():
city = session.attributes['currentCity']
url = 'http://api.openweathermap.org/data/2.5/weather?q=' + city
key = '&APPID=3d02070a84a923fe26dd362ac34ce327&units=metric&lang=en'
response = requests.get(url + key)
descr = str(response.json()['weather'][0]['description'])
temp = str(int(response.json()['main']['temp']))
response = render_template('trip_weather', city=city, description=descr, temp=temp)
return question(response)








Lastly, we have to be able to plan the trip to the wanted city, so we implement the TripGoIntent. This will finalize the session and provide a card on the Alexa app that will show when your trip is planned.

The simple card response, as displayed on the Alexa Web Application.
@ask.intent('TripGoIntent')
def trip_go():
city = session.attributes['currentCity']
response = render_template('trip_go')
return statement(reponse) \
.simple_card(title='Your trip for ' + city, content="Don't forget to catch your plane tomorrow!")




All right! The second application is all set up! Don’t forget to reload your web application. Now we just have to set up the communication part again.

The Alexa Skill — Providing Communication

Even though this second application is deployed on the same Flask-Ask service, we will create a new Alexa Skill in the Amazon Developer Console.

For Skill Information, I provided I’m going on a trip as the name and trips as the invocation name, in order to easily call out the application.

As we are using many intents, the Interaction Modelneeds quite a bit of information. First, we declare the Intent Schema as follows.

{
"intents": [
{
"intent": "TripWelcomeIntent"
},
{
"intent": "AMAZON.YesIntent"
},
{
"intent": "AMAZON.NoIntent"
},
{
"intent": "AMAZON.CancelIntent"
},
{
"intent": "AMAZON.StopIntent"
},
{
"intent": "TripProposeIntent",
"slots": [{
"name": "city",
"type": "AMAZON.EUROPE_CITY"
}]
},
{
"intent": "TripNextCityIntent"
},
{
"intent": "TripWeatherIntent"
},
{
"intent": "TripGoIntent"
}
]
}

































Next, we set up multiple Sample Utterances, so the application is able to understand various ways of speech provided by the user. Note that, like for example Thats, not everything can be written according to the correct spelling. This is because Sample Utterances do not allow certain symbols to be included in the example phrases.

TripWelcomeIntent plan a new trip
TripWelcomeIntent set up a trip
TripWelcomeIntent arrange a vacation

AMAZON.YesIntent I am ready
AMAZON.YesIntent Yes I am
TripProposeIntent I want to go to {city}
TripProposeIntent How about {city}
TripProposeIntent {city} seems nice
TripProposeIntent Can we go to {city}


TripNextCityIntent Give me another
TripNextCityIntent I want another city
TripNextCityIntent I do not want to go there
TripNextCityIntent next city


TripWeatherIntent What is the weather like over there
TripWeatherIntent How is the weather
TripGoIntent Seems great
TripGoIntent Lets go
TripGoIntent Lets go there
TripGoIntent That is good for me


Lastly, both the Configuration and SSL Certificate are set up in exactly the same way as described in application one.

Hit that Save button and the second application is all set up! You can now test it out on your favorite Echo device!

The Conclusion

It was really fun toying around with the Amazon Echo Dot. Looking back at my initial requirements, this project has been a great success.

First of all, the Alexa skills are very easily deploy-able, as you only have to import the Flask-Ask library and add a single line in front of the method you want to set up as a service.

Next, the level of complexity can be as high as you want. Because we are using Python to deploy the Alexa skills with, the possibilities are endless. Nevertheless, delay might be an issue when the computational time exceeds the expected waiting time.

Lastly, everything presented in this tutorial is offered for free and can be used to deploy your own homebrew Alexa skills with.

I am already looking forward to building more Alexa skills and seeing new functionalities being added to both Alexa and the Flask-Ask library.