Escrito por Alejandro Hernández en Planeta Chatbot.

Ayer Amazon aprobó mi Skill “Mi cartera de monedas” y después de varios procesos de revisión y cambios, me he decidido a contar mi experiencia para ahorrar un valiosísimo tiempo a la gente que se atreva con el desarrollo de una Skill para Alexa en español.

Tabla de contenidos

¿Estás empezando con Alexa?

El Hello World de Alexa es muy fácil y en 15 minutos puedes tener todo el entorno montado y conseguir que suenen las primeras palabras. Actualmente hay varias opciones para abordar el desarrollo pero os cuento las que haría yo (y en verdad he hecho).

El primer ejemplo

En este caso yo optaría sin lugar a dudas con lo que se muestra en fact-skill-1, un paso a paso muy sencillo donde te guían por todo el proceso inicial un poco más farragoso:

  • Darte de alta en la consola de Amazon (si aún no tienes una) y dar de alta tu primera Skill en la consola.
  • Crear tu primer modelo de interacción. En general yo recomiendo editar directamente el JSON en un editor externo pues me hizo más fácil el desarrollo, pero también puedes hacerlo con los diálogos de los que dispones en la web.

Intents : las acciones que vas a permitir hacer al usuario, Utterances : las diferentes expresiones que podrá usar el usuario para poder invocar las acciones, Slots : las variables que el usuario incluirá en las expresiones.

  • Implementar la lógica de tu Skill. En la mayoría de los ejemplos que podrás encontrar en el github de Amazon, se utiliza una implementación en Node.js desplegada en Lambda functions. Y la verdad es que yo también lo recomiendo dada a la sencillez del despliegue/conexión con la Skill y los recursos que hay ya sobre este lenguaje/plataforma.

Primeros tropiezos

A continuación listado de errores más comunes que he ido comentiendo a medida que he ido desarrollando la skill (seguro que vosotros sois más listos que yo, pero aquí están mis vergüenzas.)

  • Para invocar la Skill en el simulador de la consola, no es necesario incluir la palabra Alexa, y si la incluyes no debes incluir una coma después de su nombre. Es decir, si pruebas “Alexa, abre mi cartera de monedas” (Incluso aunque puedas receibir esto mismo como texto de prueba en el correo de rechazo de la Skill ante un intento de certificación) te responderá con un amable “Perdona. No he podido encontrar la respuesta a lo que me has preguntado.” Es por eso que para arrancar tu skill la primera vez que abres el simulador debes utilizar algo como esto:

“Alexa abre mi cartera de monedas”, “Abre mi cartera de monedas” o “Abre mi cartera de monedas y dime cuánto vale un bitcóin”

  • Cuidado con los acentos, si eres una persona descuidada como yo, te darás cuenta de que si cometes alguna falta de ortografía debido a incluir incorrectamente los acentos al escribir en el simulador, la skill no procesará correctamente la petición (a no ser que hayas escrito la utterance también con la misma falta de ortografía 😄). Especialmente importante cuantro tratamos con Amazon.Searchquery en lugar con un custom type en los slots.
  • Save + Build todo el rato. Cada vez que hagas un cambio, debes guardar y compilar el modelo para que esos efectos tengan consecuencia en el simulador. Y evidentemente si has hecho algún cambio en tu proyecto de node, debes volver a empaquetarlo y subirlo a tu lambda function antes de continuar.
  • El tipo de slot AMAZON.SearchQuery no puede incluirse dentro de un mismo utterance junto con un slot de otro tipo. Me temo que si incluyes ese tipo de variable (que te permite que el usuario diga la palabra que quiera) no podrás utilizar más slots o recibirás el error “Sample utterance X in intent Y cannot include both a phrase slot and another intent slot. Error code: InvalidIntentSamplePhraseSlot”
  • Puede que al principio veas que varios de tus intents repiten la misma estructura a la hora de formar la expresión “he comprado”, “he invertido”, he ganado”, “tengo una inversión de”, “me han transferido”, “acabo de comprar”, puesto que vas a intentar acaparar el mayor número de posibilidades en la expresión del usuario. Para evitar escribir mil utterances en cada intent, algo que simplificará bastante tu modelo será definir un slot con un custom type que te permita aplicar esa misma variable a varios intents.
  • El tipo de slot Amazon.number no dispone de la posibilidad de aceptar números decimales, por lo que si se quiere recoger ese tipo de información del usuario hay que utilizar una solución como la siguiente (En la que tenemos en cuenta que el usuario pueda decir “cuatro coma cinco” o “cuantro punto cinco”.)
{   "name": "GetMyCoinValueIntent",   "slots": [{   "name": "entero",   "type": "AMAZON.NUMBER"   },   {   "name": "decimales",   "type": "AMAZON.NUMBER"   },   {   "name": "moneda",   "type": "CoinType"   },   {   "name": "tengo",   "type": "IHaveType"   }   ],   "samples": [   "{tengo} {entero} punto {decimales} {moneda}",   "{tengo} {entero} coma {decimales} {moneda}"   ]  }
  • Y un último apunto sobre los números y cómo probar su funcionamiento en el simulador. Aunque pueda funcionar incluir dígitos “Calcuar el valor de 12 bitcoin”, es más recomendable que escibáis el número en texto por lo que diríamos “Calcular el valor de doce bitcoin”.

Documentación y recursos

La ejemplos y post que encuentres puede liarte un poco debido a que pueden hacer referencia a diferentes versiones de SDK. Este quizás sea el mayor problema y dolor de cabeza cuando te enfrentas al desarrollo. Hay muchos ejemplos disponibles pero pocos actualizados, tanto a nivel del modelo de interacción como el proyecto de Node.js que recibirá las peticiones. Lo mejor en este caso es ir directamente a la documentación oficial y a los ejemplos que se referencian desde ella. Os copio a continuación lo que me ayudó a completar la Skill en alguna de sus partes (tanto para la generación del modelo, como para la parte de servidor en Node.js donde quizás encontremos más confusión en relación a la última versión, 2.0, del SDK)

  • How to build an Alexa Skill in less than 30 minutes using NodeJS vídeo deAndrea Muttonique resume muy bien cómo empezar a desarrollar tu Skill.
  • Referencia en la documentación de Amazon con la que crear un modelo de conversación con la que requieras cierta información al usuario o pidas una confirmación de la misma.
  • De los ejemplos que hay actualmente publicados por Amazon, es muy recomendable echar un vistazo a petmatch.
  • Si estás preocupado sobre cómo hacer peticiones remotas desde tu Skill y no estás muy familiarizado con Node.js, yo uso 2 opciones bien conocidas promises + request, podeís encontrar más información aquí:

Es hora de empezar a conversar

Y ahora es tiempo de ver cómo pedir datos de manera progresiva a un usuario, hacerle repetir algo si no nos vale lo que ha dicho y devolver un error en el caso de no poder darle una respuesta correcta.

Pedir información a un usuario

Para poder generar un diálogo en el que pidamos datos al usuario, debemos definir los slots que serán obligatorios en el intent asociado a ese diálogo y posteriormente decidir qué le vamos a preguntar para que responda a cada uno de ellos.

"dialog": {  		"intents": [{  			"name": "GetMyCoinValueIntent",  			"confirmationRequired": false,  			"prompts": {},  			"slots": [{  					"name": "tengo",  					"type": "IHaveType",  					"confirmationRequired": false,  					"elicitationRequired": false,  					"prompts": {}  				},  				{  					"name": "moneda",  					"type": "CoinType",  					"confirmationRequired": false,  					"elicitationRequired": true,  					"prompts": {  						"elicitation": "Elicit.Intent-CoinValueIntent.IntentSlot-moneda"  					}  				},  				{  					"name": "cantidad",  					"type": "AMAZON.NUMBER",  					"confirmationRequired": false,  					"elicitationRequired": true,  					"prompts": {  						"elicitation": "Elicit.Intent-CoinValueIntent.IntentSlot-cantidad"  					}  				}  			]  		}]  	},  	"prompts": [{  			"id": "Elicit.Intent-CoinValueIntent.IntentSlot-moneda",  			"variations": [{  					"type": "PlainText",  					"value": "¿Nos puedes decir el nombre de la criptomoneda para la que quieres calcular su valor?"  				},  				{  					"type": "PlainText",  					"value": "¿Cuál es la criptomoneda para la que quieres calcular su valor?"  				}  			]  		},  		{  			"id": "Elicit.Intent-CoinValueIntent.IntentSlot-cantidad",  			"variations": [{  					"type": "PlainText",  					"value": "¿Qué cantidad tienes de la moneda?, puedes decir el número y nosotros haremos el cálculo de su valor"  				},  				{  					"type": "PlainText",  					"value": "¿Puedes decir el número de unidades que tienes de la moneda?, con ello podremos calcular su valor"  				}  			]  		}  	]

De esta manera podremos disponer de expresiones asociadas a ese intent que no contengan estas variables explícitamente, pero Alexa preguntará por aquellas que nosotros especifiquemos como obligatorias.

Recoger la información del usuario

Para poder recoger la información del usuario usaremos 2 handlers distintos, el primero para tramitar la información mientras está siendo requerida al usuario request.dialogState !== 'COMPLETED' y el segundo para tramitar el resultado final que daremos al usuario.

Primero debemos comprobar que el usuario ha respondido correctamente a las variables que le estamos requiriendo, y en caso contrario debemos volver a pedírselas.

const InProgressGetMyCoinValueHandler = {      canHandle(handlerInput) {        const request = handlerInput.requestEnvelope.request;            return request.type === 'IntentRequest'          && request.intent.name === 'GetMyCoinValueIntent'          && request.dialogState !== 'COMPLETED';      },      handle(handlerInput) {          const currentIntent = handlerInput.requestEnvelope.request.intent;          const filledSlots = currentIntent.intent.slots;          const slotValues = getSlotValues(filledSlots);            if(( slotValues.moneda.isValidated == false || slotValues.moneda.resolved === "?" ) && slotValues.moneda.isAsked == true){              return handlerInput.responseBuilder                                 .speak("Lo siento no dispongo de esa moneda, ¿Cuál es la criptomoneda para la que quieres hacer el cálculo?")                                 .reprompt("Para poder hacer el cálculo, tienes que decir el nombre de la criptomoneda")                                 .addElicitSlotDirective('moneda')                                 .getResponse()          }else if(( slotValues.cantidad.isValidated == false || slotValues.cantidad.resolved === "?" ) && slotValues.cantidad.isAsked == true){              return handlerInput.responseBuilder                                 .speak("No he entendido esa cantidad, ¿Puedes decir la cantidad para la que quieres calcular la inversión?")                                 .reprompt("Para poder hacer el cálculo, tienes que decir la cantidad que tienes de la criptomoneda")                                 .addElicitSlotDirective('cantidad')                                 .getResponse()          }else{              return handlerInput.responseBuilder              .addDelegateDirective(currentIntent)              .getResponse();          }        },    };

Para más tarde obtener la información y dar una respuesta al usuario acorde con las variables que ha introducido en la conversación. Es importante tener en cuenta que si no encontramos una respuesta correcta para el usuario debemos comunicarle el error correspondiente.

const CompletedGetMyCoinValueHandler = {      canHandle(handlerInput) {        const request = handlerInput.requestEnvelope.request;        return request.type === 'IntentRequest' && request.intent.name === 'GetMyCoinValueIntent';      },      handle(handlerInput) {            const responseBuilder = handlerInput.responseBuilder;        const filledSlots = handlerInput.requestEnvelope.request.intent.slots;        const slotValues = getSlotValues(filledSlots);          if (slotValues.moneda.resolved!== "?" && slotValues.cantidad.resolved!== "?"){          var amountCoin = parseFloat(slotValues.cantidad.resolved)          return new Promise((resolve) => {              getCoInvestment(slotValues.moneda.resolved,(price) => {                      if (price == -1) {                          const priceOutTextErr = "Lo siento, pero no he podido calcular el valor para la criptomoneda solicitada, prueba con otra moneda o inténtalo más tarde";                          resolve(responseBuilder.speak(priceOutTextErr).getResponse());                      }else{                          var finalValue = amountCoin*price                          const priceOutText = "El valor de la inversión de "+amountCoin+" "+slotValues.moneda.resolved+", actualmente es de "+finalValue.toFixed(2)+" euros";                          resolve(responseBuilder.speak(priceOutText).getResponse());                      }                  });              });        }else{          resolve(responseBuilder.speak(errorMessageGetMyCoin).getResponse());        }      },    };

Por cierto, la mejor manera de poder obtener tus variables de las respuestas del usuario es gracias a la siguiente función getSlotValues. Seguramente la encuentres en muchos ejemplos de github, en este caso he incluído una variable isAsked que me permitirá saber cuál es el slot que actualmente se está preguntando en una conversación en progreso (para poder comprobar si su valor es correcto o debemos preguntarlo de nuevo al usuario).

function getSlotValues(filledSlots) {      const slotValues = {};          Object.keys(filledSlots).forEach((item) => {        const name = filledSlots[item].name;            if (filledSlots[item] &&          filledSlots[item].resolutions &&          filledSlots[item].resolutions.resolutionsPerAuthority[0] &&          filledSlots[item].resolutions.resolutionsPerAuthority[0].status &&          filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code) {          switch (filledSlots[item].resolutions.resolutionsPerAuthority[0].status.code) {            case 'ER_SUCCESS_MATCH':              slotValues[name] = {                synonym: filledSlots[item].value,                resolved: filledSlots[item].resolutions.resolutionsPerAuthority[0].values[0].value.name,                isValidated: true,                isAsked:true              };              break;            case 'ER_SUCCESS_NO_MATCH':              slotValues[name] = {                synonym: filledSlots[item].value,                resolved: filledSlots[item].value,                isValidated: false,                isAsked: true              };              break;            default:              break;          }        } else {          slotValues[name] = {            synonym: filledSlots[item].value,            resolved: filledSlots[item].value,            isValidated: false,            isAsked: false          };        }      }, this);          return slotValues;    }

Y listo para la revisión!… o no

Antes de tener mi Skill certificada, he sufrido 5 rechazos 🙈

Morituri te salutant

Unas veces por las prisas en resolver los problemas, que evidentemente generan otros nuevos, y otras veces por puro desconocimiento. Aquí va un buen checklist para el que esté pensando pasar la revisión a su primera Skill.

  • Lo primero, echa un vistazo a este post en el queGerman Viscuso(Amazon Alexa Evangelist) nos ayuda a no cometer los errores más comunes: https://developer.amazon.com/es/blogs/alexa/post/7bb221f8-8432-4082-ad59-945ccd7dc10f/las-claves-para-certificar-con-exito-tu-skill-alexa
  • El invocationName no puede ser una única palabra a no ser que sea el nombre de tu marca o tengas los derechos en propiedad para ello.
  • Las frases de ejemplo deben contener la parte correspondiente a la invocación de la Skill, es decir, no podemos poner únicmanete “¿Cuánto vale un bitcoin?” sino que debemos optar por “Alexa, abre mi cartera de monedas y ¿Cuánto vale un bitcoin?”
  • La primera frase de ejemplo debe ser la frase de invocación de la Skill, “Alexa, abre mi cartera de monedas”
  • No podemos devolver un mensaje poco legible al usuario. Es importante que todo lo que vamos a decir al usuario tenga sentido y no sean mensajes enormes, con poca coherencia y faltas gramaticales.
  • Si incluyes una url de privacidad, debe estar enlazada con la política de privacidad que cumplimentas para tu Skill (si es necesario), tras incluir una landing genérica, decidí dejar este campo vacío para poder pasar la revisión.
  • Tienes que estar preparado para que el usuario responda cualquier palabra 😃 a las preguntas que le haces. Es por eso que debes comprobar en todo momento si su respuesta es correcta y volver a pedirlo si fuera necesario.
User: “Alexa, abre mi cartera de monedas y quiero que calcules el valor de una inversión”  Skill: “¿Cuál es la cripto moneda para la que quieres calcular su valor?”  User:”manzana”
  • Debes disponer de una respuesta de error adecuada siempre que no puedas dar otra respuesta al usuario, y en ningún caso llegar a un flujo en el que ante una petición no generes una respuesta propia al diálogo u obtendrás este error.
Skill: “Se ha producido un error con la respuesta de la Skill que has pedido”

Y si has llegado hasta aquí… gracias por leer 🙌 . Espero que te haya sido de ayuda estos tips y puedas hacer una Skill genial. Si tienes alguna pregunta que yo pueda resolver no dudes en comentarlo.

Alex.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *