Crear una skill de Alexa usando Python 2/2

Francisco Rivas
Diseñando para la Voz
25 min readOct 25, 2018

--

Hola de nuevo Exploradores!. En mi artículo anterior diseñamos el Front-End y parte del Back-End de Exploradores Fantásticos. Alexa nos asigna una misión convirtiéndonos en intrépidos y arriesgados exploradores y cazadores de objetos. Para el Front-End: decidimos el nombre de la skill, creamos la intención, los enunciados y variables. Para el Back-End: creamos la función Lambda y conectamos ambos, dejando así el camino libre para dedicarnos a preparar el entorno de desarrollo y pruebas locales usando Python. De eso justamente se trata esta segunda entrega de la serie.

¿Qué esperar de esta segunda entrega?

  • Preparación de nuestro entorno de desarrollo.
  • Desarrollo de la función Lambda asociada a nuestra skill.
  • Preparación de las pruebas locales utilizando python-lambda-local.
  • Pruebas utilizando la opción Test de AWS Lambda.
  • Pruebas en el simulador de Alexa.

Preparación de nuestro entorno de desarrollo

En este apartado describo la preparación de nuestro entorno para desarrollar nuestra skill (función Lambda).

Nuestro entorno consiste en:

  • Python 3.7.0.
  • python-lambda-local para las pruebas.
  • ASK-SDK para Python.
  • VSCode (o cualquier otro editor de tu preferencia).

Una breve nota antes de continuar:

Existen dos posibilidades para tener nuestro código fuente localmente, un directorio o virtualenv, queda totalmente a preferencia del lector utilizar uno u otro. Por razones practicas en este artículo utilizaré un directorio.

Verificando que tenemos Python instalado

Abrimos Terminal.app.

Debajo, dos formas de hacerlo.

~$ python -V
Python 3.7.0
~$ which python
/usr/bin/python

Instalando python-lambda-local para pruebas locales

Aunque las pruebas las haremos más adelante, es bueno tener todas las herramientas necesarias instaladas de antemano. Para probar nuestras funciones Lambda, HDE ha desarrollado un paquete llamado python-lambda-local, más información aquí.

Para instalar python-lambda-local hacemos lo siguiente:

  1. Abrimos Terminal.app.
  2. Verificamos que tenemos pip instalado:
~$ pip -Vpip 18.0 from /usr/local/lib/python3.7/site-packages/pip (python 3.7)

3. Instalamos python-lambda-local:

$ pip install python-lambda-local

4. Confirmamos que está instalado de cualquiera de las dos formas de debajo:

~$ python-lambda-local
usage: python-lambda-local [-h] [-l LIBRARY_PATH] [-f HANDLER_FUNCTION]
[-t TIMEOUT] [-a ARN_STRING] [-v VERSION_NAME][-e ENVIRONMENT_VARIABLES] [--version]FILE EVENTpython-lambda-local: error: the following arguments are required: FILE, EVENT~$ which python-lambda-local
/usr/local/bin/python-lambda-local

Instalando ASK-SDK para Python

El equipo de Alexa ha creado un SDK para Python, al momento de escribir este artículo en v1.0.0. Utilizar el SDK nos hace la vida bastante más sencilla a la hora de desarrollar nuestros skills y además, siendo Python tan elegante, el código resultante es muy limpio. Más información acerca del SDK aquí, incluso tiene ejemplos de skills completos, bastante útil sin duda, además lo mantienen actualizado.

Lo que vamos a hacer es instalar el SDK y cualquier otro paquete de Python que necesitemos en un directorio.

Creamos nuestro directorio, el nombre del directorio no es tan relevante:

$ mkdir alexa-skill-exploradoresfantasticos/
$ cd alexa-skill-exploradoresfantasticos/

Si utilizamos MacOS y Python instalado con Homebrew es necesario crear un archivo que permite que pip instale los paquetes en un directorio.

$ vim setup.cfg

El contenido de ese archivo es:

[install]
prefix=

Ahora podemos instalar el SDK. El flag -t permite especificar el target, es decir, donde queremos instalar el paquete.

$ pip install ask-sdk -t .
Collecting ask-sdk
Downloading https://files.pythonhosted.org/packages/ab/2e/b79c6c4e30c329fd6f5df5744dad9da5c0032c4546e6704304cd75376df3/ask_sdk-1.0.0-py2.py3-none-any.whlCollecting ask-sdk-dynamodb-persistence-adapter (from ask-sdk)Downloading https://files.pythonhosted.org/packages/43/1c/5622e86354cd731236898bfaf0c7449a9531b16c62dabf0c8cb65f540d62/ask_sdk_dynamodb_persistence_adapter-1.0.0-py2.py3-none-any.whlCollecting ask-sdk-core (from ask-sdk)Downloading https://files.pythonhosted.org/packages/f7/3b/5a140cc2877905f5e75e348a2fd8952a33e80ad929ff1adb9e77f758a3e4/ask_sdk_core-1.0.0-py2.py3-none-any.whlCollecting boto3 (from ask-sdk-dynamodb-persistence-adapter->ask-sdk)Downloading https://files.pythonhosted.org/packages/e3/af/ff24b42daacdc929629f4f85ce8a54ee1c6591475b5067d180028feffb57/boto3-1.9.28-py2.py3-none-any.whl (128kB)100% |████████████████████████████████| 133kB 5.9MB/s..... <salida elimina por ahorrar espacio>

Se instalan los siguientes paquetes y sus dependencias:

ask-sdk-1.0.0 
ask-sdk-core-1.0.0
ask-sdk-dynamodb-persistence-adapter-1.0.0
ask-sdk-model-1.2.0
boto3-1.9.28
botocore-1.12.28
certifi-2018.10.15
chardet-3.0.4
docutils-0.14
idna-2.7
jmespath-0.9.3
python-dateutil-2.7.3
requests-2.20.0
s3transfer-0.1.13
six-1.11.0
urllib3-1.24

Finalmente nuestro SDK esta compuesto de:

$ ls -1setup.cfg
six.py
__pycache__
six-1.11.0.dist-info
dateutil
python_dateutil-2.7.3.dist-info
jmespath
jmespath-0.9.3.dist-info
urllib3
urllib3-1.24.dist-info
docutils
docutils-0.14.dist-info
botocore
botocore-1.12.28.dist-info
s3transfer
s3transfer-0.1.13.dist-info
boto3
boto3-1.9.28.dist-info
ask_sdk_model
ask_sdk_model-1.2.0.dist-info
chardet
bin
chardet-3.0.4.dist-info
idna
idna-2.7.dist-info
certifi
certifi-2018.10.15.dist-info
requests
requests-2.20.0.dist-info
ask_sdk_core
ask_sdk_core-1.0.0.dist-info
ask_sdk_dynamodb
ask_sdk_dynamodb_persistence_adapter-1.0.0.dist-info
ask_sdk
ask_sdk-1.0.0.dist-info

Ahora creamos nuestro archivo principal, lo podemos llamar, por ejemplo (y para mantener la consistencia con los de JS) index.py. Me he habituado a utilizar VSCode, soy mucho más de vim, sin embargo, he descubierto que VSCode ofrece ciertas facilidades y es más ligero de lo que pensaba. No descarto mas adelante cambiarme o probar otro editor.

Recomiendo instalar el plugin de Python para VSCode ms-python.python.

Cuando ejecutamos VSCode se nos presenta la ventana de debajo, hacemos click en Add workspace folder... (1).

Agregando el directorio de la skill a VSCode

Seleccionamos el directorio en el que hemos instalado el SDK. En nuestro caso es alexa-skill-exploradoresfantasticos. En el panel de la izquierda veremos todos los directorios (paquetes) del SDK. A continuación haciendo click en (2) creamos un archivo llamado index.py que será nuestra función Lambda (3). Es importante mencionar que el nombre del archivo no es estricto, es decir, puedes utilizar el nombre que prefieras, mas adelante veremos dónde se especifica en la consola de AWS (Lambda) este nombre para que se ejecute correctamente la función.

Creando index.py: la función lambda de nuestro skill

Un poco de teoría…

by Annie Spratt on Unsplash

Cuando creamos nuestro Front-End, llamado VUI (Voice User Interface), observamos en el panel izquierdo, intenciones predefinidasAMAZON.CancelIntent, AMAZON.HelpIntent, AMAZON.StopIntent y AMAZON.NavigateHomeIntent. Adicionalmente agregamos la intención de nuestra skill ListItemsIntent.

Intenciones predefinidas y ListItemsIntent

La lista de intenciones predefinidas que ofrece Amazon es bastante grande y crece a medida que se agregan mas funcionalidades a Alexa.

Intenciones predefinidas ofrecidas por Amazon

Dependiendo de la skill que queramos desarrollar necesitaremos unas u otras, por ejemplo si queremos que nuestra skill ofrezca al usuario la posibilidad de escuchar de nuevo lo que Alexa acaba de decir se utiliza AMAZON.RepeatIntent en conjunto con AMAZON.YesIntent y AMAZON.NoIntent, si queremos darle la posibilidad al usuario de responder sí o no y estas respuestas implican otras solicitudes o transacciones en nuestra skill.

Para crear una skill personalizada debemos conocer qué es lo que espera AVS (Alexa Voice Service) de nuestra (skill) función Lambda. En principio se espera que nuestra solicitud sea un JSON con una cierta estructura, la respuesta que nos devuelve es otro JSON con otra cierta estructura, de este modo ya comprendemos que el SDK nos ayuda a manipular estos eventos (solicitudes y respuestas).

Por otro lado podemos implementar nuestra skill utilizando clases (classes) o decoradores(decorators). En este artículo utilizamos clases. Es recomendable de hecho utilizar un solo patron para toda la skill.

Se puede decir, a grandes rasgos, que cada cada una de las clases que vamos a implementar será capaz de responder a una intención (intent). Por lo que, por ejemplo, tendremos una clase llamada ListItemsIntent. De forma mas abstracta, cada solicitud (Request) que se haga a la skill debe tener un handler (clase o decorador), una forma de responder a ello, incluso si esa solicitud es desconocida debe responder apropiadamente (excepciones).

Lo que hacemos al implementar nuestra skill es derivar de las siguientes dos clases que pertenecen al paqueteask_sdk_core.dispatch_components:

  • AbstractRequestHandler: Esta clase nos permite gestionar solicitudes y responder apropiadamente a estas.
  • AbstractExceptionHandler: Esta clase nos permite gestionar las excepciones y responder apropiadamente a estas.

Cada una de las clases que vamos a implementar en nuestra skill, bien sean las personalizadas (custom) o las correspondientes a las intenciones que ofrece Amazon (AMAZON.*) implementa a su vez dos métodos:

  • can_handle: Este método devuelve true o false indicando si esta clase en particular es capaz de responder a la solicitud que esta recibiendo.
  • handle: Este método es realmente el que se encarga de preparar la respuesta (el JSON) que requiere Alexa. Es la implementación de lo que queremos que haga nuestra skill cuando reciba una solicitud en particular. En nuestro caso (que detallaremos mas abajo) implementará lo que queremos que Alexa le diga a nuestro usuario cuando solicite una cantidad de cosas para buscar. De igual manera, responde apropiadamente a una solicitud de ayuda o de repetición etc.

Las clases o decoradores que se implementan para poder gestionar las solicitudes que nuestra skill recibe se conocen como handlers, de ahí que los métodos que se implementan en cada uno lleven ese nombre.

Ahora sí, ha llegado el momento…

by Markus Spiske on Unsplash

Hagamos una pequeña lista de clases (intenciones, solicitudes) que queremos soportar en nuestra skill:

  • LaunchRequest
  • ListItemsIntent
  • HelpIntent
  • CancelIntent
  • StopIntent
  • SessionEndedRequest
  • AllExceptions

Breve nota antes de continuar:

Aunque tengamos intenciones definidas en nuestra skill que no vayamos a utilizar no significa que tengamos que implementarlas todas. Es recomendable eliminarlas si no las vamos a utilizar, en nuestro caso tendríamos que eliminar AMAZON.NavigateHomeIntent. Esto ya son deberes para casa (lector).

Breve (muy breve) explicación acerca de las intenciones y solicitudes predefinidas

Anteriormente mencioné que Amazon proporciona, hasta el momento, 24 intenciones predefinidas, explicar cada una es cuestión de otro artículo, sin embargo, si creo que es importante explicar al menos brevemente las que vamos a implementar.

Vamos a implementar las siguientes solicitudes (Request):

  • LaunchRequest: Esta solicitud es enviada la skill por el usuario sin una intención expecífica. Por ejemplo, se ejecuta cuando el usuario dice “Alexa, abre exploradores fantásticos”. En esta solicitud el usuario no especifica una intención solo pide a Alexa inicie la skill.
  • SessionEndedRequest: Es la solicitud que la skill recibe notificándole que la sesión ha sido cerrada por alguna de las siguientes razones:
  1. El usuario dice “salir”.
  2. El usuario no responde o dice algo que no coincide que ninguna de las intenciones definidas en la VUI mientras el dispositivo estaba escuchando (Voice User Interface, Font-End).
  3. Ha habido un error.

Es importante comentar dos cosas con respecto a esta solicitud:

  1. Es distinta de AMAZON.CancelIntent y AMAZON.StopIntent en el sentido de que no se puede incluir texto, voz o una tarjeta.
  2. Se puede utilizar para hacer limpieza u otras tareas del tipo en la skill.

Por otro lado tenemos las intenciones:

  • AMAZON.HelpIntent: Esta intención se envía cuando el usuario dice, por ejemplo: “ayuda”, “ayúdame”, “puedes ayudarme”.
  • AMAZON.CancelIntent: Esta intención se envía cuando el usuario dice, por ejemplo: “cancela”, “olvídalo”, “cancela eso”. Procesando esta intención podemos dejar que el usuario cancele una acción manteniéndose en la skill o podemos dejar que el usuario salga completamente de esta.
  • AMAZON.StopIntent: Esta intención se envía cuando el usuario dice, por ejemplo: “para”, “apagar”, “cállate”. Procesando esta intención podemos dejar que el usuario detenga una acción manteniéndose en la skill o podemos dejar que el usuario salga completamente de esta.

Con respecto a las excepciones, existen al menos un par de formas de implementar esto, sin embargo, estamos dando los primeros pasos así que vamos a implementar las más sencilla, sin embargo, bastante funcional. Veremos mas adelante que las excepciones derivan de una clase distinta de las solicitudes (Request) se llama AbstractExceptionHandler.

Finalmente, nuestra intención personalizada ListItemsIntent. Esta la veremos con mas detalle debajo, por el momento nos basta con saber que deriva de AbstractRequestHandler como las intenciones predefinidas, por tanto requiere que implementemos los métodos can_handle y handle, como mencioné mas arriba.

Hasta aquí la teoría, vamos a la practica

by Joshua Jamias on Unsplash

El código de la función lambda que vamos a implementar esta disponible en mi repositorio de GitHub, sin embargo te recomiendo escribirlo todo, aunque tengas errores porque así se aprende mejor que copiando y pegando; además te habitúas a ver los posibles errores que puedes cometer.

En nuestro archivo index.py que tenemos abierto en nuestro editor favorito y está en blanco vamos a importar diferentes módulos del SDK que necesitamos.

Empezamos importando SkillBuilder. Es una clase que pertenece al paquete ask_sdk_core.skill_builder que nos permite registrar (internamente las agrega a un arreglo) nuestros handlers (classes que nos permiten gestionar las intenciones, solicitudes) (Línea 3).

Lo siguiente son las clases AbstractRequestHandler y AbstractExceptionHandler que, como mencioné mas arriba son las clases que le permiten a la skill realmente funcionar (Línea 4) .

Necesitamos otras clases utilitarias, por ejemplo is_request_type y is_intent_name que nos permiten determinar si una solicitud es una instancia de una solicitud (Request) o una intención (Intent) (Línea 5).

También la clase HandlerInput, es el objeto global que reciben las clases AbstractRequestHandler y ExceptionRequestHandler que expone métodos que también vamos a utilizar, como por ejemplo: request_envelope y response_builder(Línea 6).

Finalmente, importamos otros paquetes que nos dan ciertas utilidades como (Líneas 7 y 8):

  • logging: Nos permite mostrar la información que consideremos importante en los logs.
  • six: es una libreria de compatibilidad. Ofrece utilidades que funcionan en Python 2 y Python 3.
index.py — Parte 1

Breve nota antes de continuar:

Los nombres de las variables no siguen ninguna convención especial o sugerida por el equipo de Alexa de Amazon. En caso de que exista alguna, haré el comentario.

Lo siguiente que haremos será crear el objeto SkillBuilder que nos permite agregar nuestras clases (handlers). Es como decirle a Lambda, de lo que es capaz nuestra skill.

Creamos nuestro objeto logger que nos va a permitir ir mostrando información durante las pruebas tanto locales como en el simulador y luego los logs de ejecución de la skill.

Los niveles mas importantes que soporta logger son:

  • CRITICAL
  • DEBUG
  • ERROR
  • FATAL
  • INFO
  • WARN o WARNING

Sugiero que al momento de subir tu skill a Lambda lo cambies a INFO, es mas que suficiente.

Implementando LaunchRequestHandler

Ahora vamos a crear la clase LaunchRequestHandler que, como mencioné antes deriva de AbstractRequestHandler por lo que debemos también implementar los métodos can_handle y handle:

Lo primero en lo que nos vamos a fijar es que, en efecto, LaunchRequestHandler deriva de AbstractRequestHandler y ademas implementamos can_handle y handle. En ambos se recibe handler_input como argumento. Este argumento contiene toda la información sobre la intención/solicitud que ha recibido la skill, por ejemplo: el contexto, valores de las variables (slots), etc. Es por esto que en can_handle utilizamos is_request_type para determinar si esta clase (handler) es capaz de responder a la solicitud LaunchRequest. Lo mismo sucede con las otras clases e intenciones.

Ahora, en lo que se refiere a handle, debido a que esta clase se encarga de responder a una solicitud de inicio de la skill, lo que vamos a hacer es saludar al usuario. En esta clase también podríamos, por ejemplo, verificar si tenemos atributos.

Nos fijamos en la línea 6, observamos que asignamos lo que queremos que Alexa le diga al usuario cuando inicie la skill sin ninguna intención, es decir, utilizando simplemente el nombre de apertura: “Alexa, abre exploradores fantásticos”. También observamos esto <say-as interpret-as=\"interjection\">Hey Exploradores!</say-as> esta es una de las cosas mas útiles que ofrece Alexa; es la capacidad de entonar, cambiar el ritmo, volumen, forma de pronunciación de frases, letras, expresiones etc. En este caso utilizamos interjection que da cierto énfasis al texto que queremos pronunciar. Más información acerca de esto aquí y más sobre SSML (Speech Synthesis Markup Language).

Por último lo que hacemos es devolver nuestra respuesta, con la ayuda del SDK construimos el JSON de respuesta. Observemos dos métodos speak y ask :

  • speak(self, speech): Es un método que recibe el texto que queremos que Alexa diga. Línea 6
  • ask(self, reprompt): Es un método que hace que Alexa, pasados 8 segundos sin respuesta, diga el texto que recibe, si te fijas los mensajes son distintos, el que recibe este método es como para reforzar la pregunta o de forma sutil recordar al usuario que la skill espera una respuesta. Línea 7

Finalmente set_should_end_session(Bool) recibe True o False indicándole a Alexa si debe o no cerrar la sesión luego de responder la solicitud LaunchRequest en todos los casos, a menos que exista un error en alguna tarea adicional que se realice durante el lanzamiento o inicio de la skill, debemos ponerlo a False.

Implementando HelpIntentHandler

Continuamos con la intenciónHelpIntent que , como mencioné antes, se encarga de gestionar la respuesta a la solicitud AMAZON.HelpIntent (Línea 3) que sucede cuando el usuario dice, por ejemplo, ayuda. De igual manera de implementan can_handle y handle. En este caso solo utilizamos speak puesto que no esperamos que el usuario responda a ninguna pregunta (Línea 8), que no quiere decir que no pueda implementarse, por ejemplo para preguntar si la ayuda proporcionada ha resulto su duda. También es importante notar, que, al no utilizar set_should_end_session la sesión automáticamente se cierra. Esto queda a decisión del diseñador de la skill.

Es importante fijarnos en la Línea 3, utilizamos is_intent_name puesto que nos interesa que el valor que recibimos sea del tipo intención. De hecho internamente is_intent_name utiliza isintance para verificar que la solicitud (Request) es del tipo IntentRequest.

Se recomienda que el mensaje que utilicemos proporcione al usuario algún enunciado que no esté en los ejemplos de la skill en la tienda, también proporcionar otras formas de obtener ayuda sobre alguna otra funcionalidad de la skill.

Implementando CancelAndStopIntentHandler

Esta clase va a gestionar dos intenciones AMAZON.CancelIntent y AMAZON.StopIntent, por qué?, buena pregunta, porque nuestra skill es bastante simple, en primer lugar; porque usualmente en una skill que no utiliza persistencia o se realizan tareas mas complejas; que el usuario diga “para”, “olvídalo” o “cancela” para nosotros significa lo mismo, queremos detener cualquier cosa Alexa esté haciendo en ese momento.

Si nos fijamos en la Línea 3 observamos que verificamos cualquiera de las dos intenciones. No realizamos ninguna tarea adicional así que simplemente pedimos a Alexa que diga algo y cerramos la skill.

Implementando SessionEndedRequestHandler

Esta clase gestiona SessionEndedRequest que, como mencioné antes, es enviada a la skill cuando el usuario no responde o responde algo que no es reconocido por la skill o simplemente hay un error. Cerrar la skill de esta forma no permite la utilización de speak o cards (para enviar información a la aplicación de Alexa del móvil).

Implementando AllExceptionsHandler

Esta clase gestiona las excepciones, se puede decir que es un CatchAll para lo que sea que la skill no comprenda. Es por esto que simplemente devolvemos True en can_handle indicando que gestionamos cualquier cosa. Además si nos fijamos en el mensaje de respuesta (Línea 6) simplemente expresamos que no comprendemos lo que nos dicen y cerramos la sesión. Esto sucede cuando ninguna de las otras clases handlers es capaz de responder a la solicitud.

Finalmente implementamos nuestra maravillosa clase ListItemsIntent

Hemos llegado a la clase que define realmente lo que hace nuestra skill.

Primero explicaré cómo espero que funcione handle. Hay dos escenarios que debemos gestionar.

  1. Cuando el usuario efectivamente proporciona un número y ya sabemos cuantos objetos debemos extraer de nuestro arreglo de forma aleatoria para devolverlos.
  2. Cuando el usuario utiliza alguna de los enunciados en los que no se especifica la cantidad y entonces podemos hacer dos cosas: generar un número aleatorio o devolver un número fijo. En nuestro caso para mantener la simplicidad de la skill vamos a devolver un número que estará definido por nosotros, que será 3.

Nota antes de continuar:

La implementación de esta clase es la más simple posible a efectos de explicar el funcionamiento de la skill. Ciertamente se puede implementar de, al menos, 2 formas más.

Si observamos el JSON en la Línea 32 vemos la etiquetarequest dentro de esta intent (Línea 37) y dentro tenemos name (Línea 38) vemos el nombre de nuestra intención y esto es justamente lo que can_handle compara y finalmente slots (Línea 39) que contiene numObj (Línea 40) que a su vez contiene name y value (Líneas 41 y 42).

Ahora que comprendemos la estructura del JSON que recibimos implementamos la clase.

Nota importante antes de continuar:

No nos preocupemos ahora mismo por saber de dónde he sacado esa solicitud de ListItemsIntent puesto que lo explicaré más adelante con las pruebas.

Primero, en can_handle (Línea 3) utilizamos is_intent_name para confirmar que podemos gestionar la solicitud del usuario que correspondiente.

Antes de continuar necesitamos importar un paquete que nos va a permitir obtener elementos aleatorios de nuestro arreglo de objetos a buscar

Con respecto a handle:

  • Línea 6: Observamos que utilizamos handler_input para obtener la información de las variables que vienen en la solicitud que ha recibido la skill. Como mencioné antes, handler_input expone request_envelope que tiene toda la estructura e información de la solicitud.
  • Línea 7: Declaramos y definimos una variable que tendrá el número por defecto de objetos a extraer del arreglo de objetos a buscar por el usuario.
  • Línea 9: Lo que hacemos es iterar sobre slots que contiene la información que nos interesa obtener del usuario. Utilizamos el iterador que ofrece six esto nos devuelve dos cosas: el nombre de la variable y la información que pertenece a la misma.
  • Línea 10: Verificamos que el nombre de la variable sobre la que estamos trabajando es numObj, es decir, que corresponde a la variable que definimos, como no hay otro, este paso podría ser considerado innecesario, sin embargo, es mejor asegurarnos.
  • Línea 11: Verificamos que la variable numObj tiene un objeto value. Si nos fijamos en el JSON mas arriba observamos que en la Linea 42 value tiene el valor 3. Entonces recibiendo ese JSON esto sería True por lo que procedemos a la Línea 12.
  • Línea 12: Utilizamos sample para extraer la cantidad de elementos solicitados por el usuario de forma aleatoria. Convertimos el valor que se encuentra en currentSlot.value a int por asegurarnos de que tenemos pasamos a sample un número, aunque no es necesario porque Python hace muy buena inferencia con los tipos de datos primitivos. El resultado de esta operación la guardamos en objsToSearch.
  • Línea 14: La operación que realizamos aquí es muy similar a la de la Línea 12 ya que utilizamos la misma función sólo que en este caso pasamos nuestro valor por defecto defaultObjsToSearch y además porque no existe el objeto value de nuestra variable (slot) numObj.
  • Línea 15: Definimos el texto que queremos devolver a Alexa para que lo diga al usuario.
  • Líneas 16 y 17: Utilizamos nuestro logger para imprimir en el log (que veremos mas adelante).
  • Línea 19: Finalmente devolvemos la respuesta con el texto.

Ahora, ya teniendo todas las clases (handler):

  • Líneas 1–7: las registramos en el SkillBuilder y
  • Línea 9: posteriormente agregamos este al lambda_handler. Este es un método que sirve de puerta de entrada para el servicio Lambda, le estamos diciendo a Lambda que es lo que esta disponible. Por lo que si omitimos registrar alguna clase simplemente Lambda no la toma en cuenta y nunca la va a ejecutar. Internamente add_request_handler primero verifica que el argumento que le estamos pasando es, en efecto, una instancia , del tipo, AbstractRequestHandler y lo agrega a un arreglo que luego Lambda comprende. Lo mismo sucede con add_exception_handler solo que se verifica que es instancia de AbstractExceptionHandler.

Continuando en la Línea 9 es el nombre de esta variable el que necesitamos especificar cuando subamos el código de nuestra función al servicio Lambda. Esto lo veremos con detalle mas abajo.

Para finalizar esta sección debajo coloco el arreglo de objetos a buscar

¿Habrá que probarla no?

by Nicolas Thomas on Unsplash

Este es un tema bastante interesante porque vamos a probarlo de 3 formas:

  1. Pruebas locales utilizando python-lambda-local
  2. Probarlo utilizando la opción Test del servicio Lambda
  3. Probarlo en el simulador de la consola de desarrolladores de Alexa

Probando nuestra función Lambda localmente

Para ello, como mencioné, vamos a utilizar python-lambda-local. Para utilizar esta aplicación debemos tener a mano las solicitudes (JSON) que representan los posibles escenarios de utilización de nuestra skill. La ventaja de probar localmente es que ahorramos bastante tiempo depurando cuando hay errores y vemos los errores directamente en nuestra terminal.

Para ello vamos a utilizar las que están disponibles en Lambda.

Primero, vamos a la consola de AWS, al servicio Lambda

Configurando los tests

Seleccionamos Configure test events (1). Luego desplegamos las opciones (3) y seleccionamos Amazon Alexa Start Session (3).

Seleccionamos Amazon Alexa Start Session

Luego asignamos un nombre a nuestro evento (4) y copiamos todo el JSON (5) y finalmente guardamos este evento (6) que nos va a servir en un futuro bastante cercano.

Podemos fijarnos que en el JSON de la imagen de debajo en la Línea 33 tenemos el tipo de solicitud que se está haciendo LaunchRequest y nuestra skill está preparada para ello.

Creando nuestro primer evento para LaunchRequest

Abrimos la Terminal.app y vamos al directorio donde tenemos nuestro index.py y hacemos lo siguiente (puedes utilizar tu editor preferido):

$ mkdir tests
$ vim tests/LaunchRequest.json

Presionamos i y pegamos el JSON que hemos copiado.

Presionamos esc :wq para guardar y salir. Verificamos que están guardados los cambios haciendo:

$ head tests/LaunchRequest.json
{
"version": "1.0",
"session": {
"new": true,
"sessionId": "amzn1.echo-api.session.123456789012",
"application": {
"applicationId": "amzn1.ask.skill.987654321"
},
"user": {
"userId": "amzn1.ask.account.testUser"....<salida reducida>

¿Cuál es el resultado esperado de esta primera prueba?

Esperamos el siguiente mensaje, que ha sido el que definimos:

<say-as interpret-as="interjection">Hey Exploradores!</say-as>, espero estéis listos para una nueva aventura. ¿Cuántos objetos queréis buscar hoy?.

Probemos 🥁:

$ python-lambda-local index.py -f handler tests/LaunchRequest.json[root - INFO - 2018-10-23 19:24:02,289] Event: {'version': '1.0', 'session': {'new': True, 'sessionId': 'amzn1.echo-api.session.123456789012', 'application': {'applicationId': 'amzn1.ask.skill.987654321'}, 'user': {'userId': 'amzn1.ask.account.testUser'}, 'attributes': {}}, 'context': {'AudioPlayer': {'playerActivity': 'IDLE'}, 'System': {'application': {'applicationId': 'amzn1.ask.skill.987654321'}, 'user': {'userId': 'amzn1.ask.account.testUser'}, 'device': {'supportedInterfaces': {'AudioPlayer': {}}}}}, 'request': {'type': 'LaunchRequest', 'requestId': 'amzn1.echo-api.request.1234', 'timestamp': '2016-10-27T18:21:44Z', 'locale': 'es-ES'}}[root - INFO - 2018-10-23 19:24:02,289] START RequestId: 06bedd50-286e-47ae-bbf9-ed396cbc374c[root - INFO - 2018-10-23 19:24:02,334] END RequestId: 06bedd50-286e-47ae-bbf9-ed396cbc374c[root - INFO - 2018-10-23 19:24:02,334] RESULT:{'version': '1.0', 'sessionAttributes': {}, 'userAgent': 'ask-python/1.0.0 Python/3.7.0', 'response': {'outputSpeech': {'type': 'SSML', 'ssml': '<speak><say-as interpret-as="interjection">Hey Exploradores!</say-as>, espero estéis listos para una nueva aventura. ¿Cuántos objetos queréis buscar hoy?.</speak>'}, 'reprompt': {'outputSpeech': {'type': 'SSML', 'ssml': '<speak><say-as interpret-as="interjection">Venga exploradores!</say-as>. A la aventura, ¿Cuantos objetos queréis buscar hoy?</speak>'}}, 'shouldEndSession': False}}[root - INFO - 2018-10-23 19:24:02,335] REPORT RequestId: 06bedd50-286e-47ae-bbf9-ed396cbc374c Duration: 44.19 ms

Bien, vamos por partes

  1. Sobre el comando:
  • El primer parámetro que pasamos es el nombre del archivo que tiene nuestras clases index.py.
  • Luego con la bandera -f especificamos el nombre del handler. Mas arriba mencioné que definíamos una variable llamada handler que sera la puerta de entrada del servicio Lambda, que es cómo le dejamos saber a Lambda lo que nuestra skill es capaz de hacer. Pues el nombre de esa variable (puerta de entrada) va ahí.
  • Finalmente la ruta y nombre del archivo que contiene el evento (solicitud) que le hacemos a la skill.

2. Lo siguiente que nos muestra la salida es justamente el evento.

3. Mas abajo podemos ver la respuesta de nuestra skill, específicamente de nuestra clase LaunchRequestHandler. También en negrita observamos que se imprime el mensaje que queremos dentro de los tags <speak></speak> esto es lo que el SDK nos ayuda a construir, utilizando SSML. De hecho podemos ver también el reprompt.

{
"version": "1.0",
"sessionAttributes": {},
"userAgent": "ask-python/1.0.0 Python/3.7.0",
"response": {
"outputSpeech": {
"type": "SSML",
"ssml": "<speak><say-as interpret-as="interjection">Hey Exploradores!</say-as>, espero estéis listos para una nueva aventura. ¿Cuántos objetos queréis buscar hoy?.</speak>"
},
"reprompt": {
"outputSpeech": {
"type": "SSML",
"ssml": "<speak><say-as interpret-as="interjection">Venga exploradores!</say-as>. A la aventura, ¿Cuantos objetos queréis buscar hoy?</speak>"
}
},
"shouldEndSession": "False"
}
}

Ha sido un éxito la prueba ✅

Vamos más allá, que nos venimos arriba…

Probemos ahora con el mismo procedimiento anterior de crear un evento en la consola de AWS del servicio Lambda, sólo que esta vez creamos un IntentRequest utilizando la plantilla (Event template ) Amazon Alexa Intent Recipe.

Le asignamos un nombre, puede ser el que prefieras, en mi caso, para identificarlo mejor decidí queListItemsIntent estaría bien (4).

Observamos que aquí debemos hacer algunos cambios (5):

  • Línea 38: Cambiamos RecipeIntent por ListItemsIntent.
  • Línea 40: Cambiamos Item por numObj.
  • Línea 41: Cambiamos el nombre Item por numObj.
  • Línea 42: Cambiamos snowball por 3 (o el número de nuestra preferencia)

Seleccionamos todo y lo copiamos y pegamos en un archivo llamado, por ejemplo ListItemsIntent.json dentro de nuestro directorio tests.

Configurando ListItemsIntent

Probemos 🥁:

Podemos ver que el comando es el mismo solo cambiamos el evento que enviamos a nuestra skill.

Luego podemos ver ['bote de crema dental', 'percha', 'mopa'] que son los 3 elementos aleatorios que hemos extraído. Además el mensaje que vamos a devolver para que Alexa lo diga. Esto corresponde a las salidas de nuestro logger (Líneas 16 y 17) de nuestra clase ListItemsIntent.

$ python-lambda-local index.py -f handler tests/ListItemsintent.json[root - INFO - 2018-10-23 19:49:56,356] Event: {'version': '1.0', 'session': {'new': False, 'sessionId': 'amzn1.echo-api.session.123456789012', 'application': {'applicationId': 'amzn1.ask.skill.987654321'}, 'attributes': {}, 'user': {'userId': 'amzn1.ask.account.testUser'}}, 'context': {'AudioPlayer': {'playerActivity': 'IDLE'}, 'System': {'application': {'applicationId': 'amzn1.ask.skill.987654321'}, 'user': {'userId': 'amzn1.ask.account.userId'}, 'device': {'supportedInterfaces': {'AudioPlayer': {}}}}}, 'request': {'type': 'IntentRequest', 'requestId': 'amzn1.echo-api.request.1234', 'timestamp': '2016-10-27T21:06:28Z', 'locale': 'en-US', 'intent': {'name': 'ListItemsIntent', 'slots': {'numObj': {'name': 'numObj', 'value': '3'}}}}}[root - INFO - 2018-10-23 19:49:56,357] START RequestId: 88599520-5c86-4b25-b194-69b352d29c3f[request-88599520-5c86-4b25-b194-69b352d29c3f - INFO - 2018-10-23 19:49:56,397] ['bote de crema dental', 'percha', 'mopa'][request-88599520-5c86-4b25-b194-69b352d29c3f - INFO - 2018-10-23 19:49:56,397] <say-as interpret-as="interjection">Magnífico!</say-as>. Aquí van, prestad atención: bote de crema dental, percha, mopa. A divertirse!. <say-as interpret-as="interjection">Suerte!</say-as>.[root - INFO - 2018-10-23 19:49:56,398] END RequestId: 88599520-5c86-4b25-b194-69b352d29c3f[root - INFO - 2018-10-23 19:49:56,398] RESULT:{'version': '1.0', 'sessionAttributes': {}, 'userAgent': 'ask-python/1.0.0 Python/3.7.0', 'response': {'outputSpeech': {'type': 'SSML', 'ssml': '<speak><say-as interpret-as="interjection">Magnífico!</say-as>. Aquí van, prestad atención: bote de crema dental, percha, mopa. A divertirse!. <say-as interpret-as="interjection">Suerte!</say-as>.</speak>'}}}[root - INFO - 2018-10-23 19:49:56,398] REPORT RequestId: 88599520-5c86-4b25-b194-69b352d29c3f Duration: 40.47 ms

Ha sido un éxito la prueba ✅

Probando nuestra función en la nube

Para poder realizar las pruebas utilizando Test del servicio Lambda debemos primero preparar nuestra función.

Por requerimientos del servicio Lambda, debemos crear un archivo .zip, podemos llamarlo como queramos, preferiblemente que no tenga espacios (mala costumbre).

En nuestro caso lo llamamos exploradoresfantásticos.zip. Abrimos Terminal.app y creamos el archivo comprimido. Por supuesto si prefieren hacerlo utilizando la interfaz gráfica o alguna otra herramienta de compresión, con total libertad.

Es muy importante comentar qué es lo que vamos a incluir en este archivo. La respuesta rápida es: todo lo que esta dentro del directorio del proyecto (ask-sdk, index.py, etc), sin embargo podríamos encontrarnos en una situación en la que creamos directorios para pruebas (como será nuestro caso).

$ zip -r exploradoresfantasticos.zip * -xtests/* -xREADME* -x\__*
adding: ask_sdk/ (stored 0%)
adding: ask_sdk/standard.py (deflated 70%)
adding: ask_sdk/__init__.py (deflated 37%)
adding: ask_sdk/__version__.py (deflated 45%)
adding: ask_sdk/__pycache__/ (stored 0%)adding: ask_sdk/__pycache__/__version__.cpython-37.pyc (deflated 32%)
....<salida reducida>

Nota importante antes de continuar:

No existe un espacio en blanco entre -x y el directorio/archivo que queremos excluir de nuestro archivo comprimido. En mi caso, he creado un directorio de pruebas, un README.md que corresponde a mi GitHub y luego directorios de pip __pycache__ .

También podemos confirmar los archivos/directorios que han sido incluidos en nuestro exploradoresfantásticos.zip así:

$ unzip -vl exploradoresfantasticos.zip
Archive: exploradoresfantasticos.zip
Length Method Size Cmpr Date Time CRC-32 Name
-------- ------ ------- ---- ---------- ----- -------- ----
0 Stored 0 0% 10-20-2018 16:54 00000000 ask_sdk/
4188 Defl:N 1274 70% 10-20-2018 16:54 304f224f ask_sdk/standard.py
593 Defl:N 373 37% 10-20-2018 16:54 3a388fca ask_sdk/__init__.py
1171 Defl:N 646 45% 10-20-2018 16:54 703c3845 ask_sdk/__version__.py
0 Stored 0 0% 10-20-2018 16:54 00000000 ask_sdk/__pycache__/
781 Defl:N 531 32% 10-20-2018 16:54 5262bb3b ask_sdk/__pycache__/__version__.cpython-37.pyc
....<salida reducida>

El siguiente paso ahora es subir nuestro archivo a Lambda y hacer una primera prueba. Afortunadamente al crear los eventos ya tenemos la mismas dos pruebas que hemos realizado localmente. Es interesante hacer este ejercicio por 2 razones:

  1. Podemos confirmar Lambda es capaz de ejecutar perfectamente nuestra función. Me he encontrado en situaciones donde faltan librerías o paquetes por lo que tengo que instalarlos y subirlos junto a mi index.py.
  2. Tenemos acceso a CloudWatch Logs que nos permite ver las salidas de nuestra función como las vemos en nuestra terminal por eso utilizamos un logger.

Vamos a ello:

  • Lo primero que hacemos es click en (1) observamos que aparece el panel de debajo, hacemos click en el menú desplegable de Code entry type(2) seleccionamos la opción Upload a .zip file.
  • Luego seleccionamos el Runtime que es Python 3.6, seguimos con Handler que es la “puerta de entrada” de Lambda a nuestra función (skill) por eso ponemos index.handler(4), siendo index el nombre de nuestro archivo sin la extensión seguido por la variable que hemos definido para almacenar las clases (o handlers) que hemos agregado a nuestro SkillBuilder.
  • Hacemos click en Upload (5) que nos permite seleccionar el archivo comprimido de nuestra función (skill) llamado, en mi caso, exploradoresfantásticos.zip.
  • Finalmente hacemos click en Save (6).
Subiendo nuestro archivo ZIP con la función Lambda de nuestra skill

Una vez que este proceso ha terminado, que no debería tardar más de unos 15 segundos. Seleccionamos el evento que queremos que nuestra skill gestione (7) en nuestro caso seleccionamos LaunchRequest y hacemos click en Test (8).

Seleccionamos LaunchRequest

El resultado lo vemos en la misma ventana. Nuestra prueba ha sido correcta. Podemos que la salida es la misma que en nuestra prueba anterior utilizando la terminal.

Resultado de la prueba con LaunchRequest

Ha sido un éxito la prueba ✅

Nota antes de continuar:

Deberes: hacer la prueba con el otro evento ListItemsIntent. Si tienen algún problema no duden en contactarme.

CloudWatch Logs

Si observamos en la sección de resultados de nuestra prueba podemos ver la palabra logs (1).

CloudWatch Logs

Haciendo click en (1) se abre otra pestaña, en la que aparecen los distintos eventos ocurridos en relación a esa función, ordenados por fecha y hora, hacemos click en el que tengamos disponible y vemos algo como la imagen de debajo, he resaltado esas dos salidas puesto que son las que nuestro logger produce tanto en la terminal como en los logs. Es importante resaltar que estos logs están disponibles cuando se hacen pruebas utilizando Lambda en la nube como cuando utilizamos el simulador.

Probamos utilizando el simulador

Como pre-requisíto para estas pruebas debemos tener nuestro modelo construido. En la Consola de desarrolladores de ASK click en Build y vemos la opción Build Model (1).

Construyendo el modelo de interacción

Luego hacemos click en Test (1), vemos que las pruebas para nuestra skill están desactivadas, simplemente las activamos (2) y utilizando el pequeño icono de un micrófono (3), debemos dejarlo pulsado al dar la orden, como un walkie-talkie. Podemos observar en el panel de la izquierda (4) y (5) nuestro enunciado y luego la respuesta de Alexa, que corresponde con lo que esperamos. En el panel de la derecha podemos ver el JSON que corresponde al evento que recibe nuestra skill y la salida que producimos. Todo corresponde con nuestras pruebas anteriores, solo que ahora hasta escuchamos por ejemplo lasinterjections funcionando y notamos cómo Alexa cambia un poco el tono de la voz.

Primera prueba: LaunchRequest

Ahora respondemos a lo que Alexa nos solicita, de nuevo utilizando el micrófono (6). En esta prueba sí proporcionamos un numero. Observamos también su respuesta con la cantidad aleatoria de objetos.

Respondiendo a la solicitud de Alexa

Ahora probemos dejando que Alexa decida la cantidad de objetos. Como hemos establecido en nuestra skill, dejando que Alexa decida la cantidad de objetos, implica que va a extraer 3, y es esta la respuesta que recibimos.

Dejando que Alexa decida la cantidad de objetos

Ha sido un éxito estas pruebas también ✅

Deberes…sí, ahora hay que practicar

  1. Intenta implementar que el número de objetos que la función extrae cuando el usuario no proporciona un número sea aleatorio también, para ello puedes utilizar las funciones de random. Ten cuidado que el número sea razonable, por ejemplo utiliza un rango del 1 al 7 o algo asi.
  2. Intenta que el arreglo de objetos a buscar no esté en una variable dentro del código fuente, ponlo en un archivo y que la función lo lea, así será más fácil agregar objetos.
  3. Verificar que cuando hagas las pruebas con el simulador veas las entradas en CloudWatch Logs.

Ya para finalizar, el proceso de desarrollo de una skill entera es muy interesante. Recuerda que consta de dos partes la VUI (Front-End), Función Lambda (y otros servicios como S3, IAM, entre otros que se conoce como Back-End). La VUI consiste en el nombre de apertura, enunciados, intenciones, variables, entre otros. Es importante diseñar bien y con calma el modelo de conversación e interacción entre Alexa y el usuario, es como el script de una película. Luego con respecto al Back-End recuerda implementar las clases necesarias para gestionas los distintos eventos que puede recibir tu skill y sobre todo registrarlos en el SkillBuilder. Hay 3 formas de probar tu función Lambda: local, con el servicio Lambda en la nube y en el simulador; ten en cuenta en cualquier caso que tienes logs disponibles incluso a través de CloudWatch. Ahora, ¡a explorar! :D

Gracias por leer,

Escribió para Diseñando para la Voz, Francisco

Si tienes dudas, sugerencias, comentarios o quejas, no dudes en contactarme. También puedes encontrarme en Twitter en @franciscorivash.

--

--

Francisco Rivas
Diseñando para la Voz

Alexa Developer | Coffee Enthusiast | Percussionist | Curious (life, tech) | Keen on learning