Criando uma Skill para a Amazon Alexa

Arquitetura e modelo de interações da “Skill” Alexa que reproduz os Drops da EximiaCo

Implementar um skill para Amazon Alexa é uma atividade relativamente simples. Tudo começa com o desenvolvimento de um modelo de interações, conforme conceitos que destacamos no post anterior. Na skill da EximiaCo, construímos este modelo diretamente no console de desenvolvimento da Amazon Alexa. Entretanto, também seria possível utilizar o CLI do SDK da Alexa, o ASK, para criar e publicar a skill.

Para o backend, há duas opções de endpoint  disponíveis: AWS Lambda (recomendada) e HTTPS. Optamos por desenvolver uma Lambda em Python utilizando o AWS SAM.

Em conjunto com a Lambda incluímos os demais recursos de infraestrutura existentes na arquitetura da skill, como tabelas do DynamoDB, um tópico do SNS, e outra Lambda que possui a função de Crawler, extraindo os dados dos episódios do feed do SoundCloud.  Essa arquitetura permite que, de forma simples, possamos publicar a aplicação em qualquer conta da AWS, com apenas alguns comandos (IaC).

A construção do handler da Lambda é feita selecionando um dos Builders (Pattern) fornecidos pelo ASK. Cada builder tem características e suportes diferentes. Em nossa skill utilizamos o StandardSkillBuilder, que possui suporte a armazenamento persistente de estado utilizando uma tabela no DynamoDB. Desta forma, tivemos a possibilidade de armazenar o estado do playback, permitindo que a reprodução continuasse de onde parou, quando o usuário retomasse a reprodução.

import os

from ask_sdk.standard import StandardSkillBuilder

from command_handlers import (LaunchRequestHandler, StartLatestEpisodeHandler, StopEpisodeHandler, SearchEpisodeHandler,
                              ShuffleOnEpisodeHandler, ShuffleOffEpisodeHandler, StartOverEpisodeHandler,
                              RepeatEpisodeHandler,ResumeEpisodeHandler, HelpHandler, FallbackHandler, SessionEndedRequestHandler,
                              PreviousEpisodeHandler, NextEpisodeHandler, LoopOnEpisodeHandler, LoopOffEpisodeHandler)
from event_handlers import (PlaybackStoppedHandler, PlaybackFinishedHandler, PlaybackStartedHandler,
                            PlaybackFailedHandler, PlaybackNearlyFinishedHandler)
from interceptors import (SaveStateResponseInterceptor, LoadStateRequestInterceptor, RequestLogger, ResponseLogger,
                          CatchAllExceptionHandler)

streaming_table_name = os.getenv("STREAMING_TABLE_NAME")
skill_builder = StandardSkillBuilder(
    table_name=streaming_table_name,
    auto_create_table=False)

# Launch Request
skill_builder.add_request_handler(LaunchRequestHandler())

# Command Handlers
skill_builder.add_request_handler(StartLatestEpisodeHandler())
skill_builder.add_request_handler(SessionEndedRequestHandler())
skill_builder.add_request_handler(StopEpisodeHandler())
skill_builder.add_request_handler(ResumeEpisodeHandler())
skill_builder.add_request_handler(SearchEpisodeHandler())
skill_builder.add_request_handler(ShuffleOnEpisodeHandler())
skill_builder.add_request_handler(ShuffleOffEpisodeHandler())
skill_builder.add_request_handler(StartOverEpisodeHandler())
skill_builder.add_request_handler(RepeatEpisodeHandler())
skill_builder.add_request_handler(PreviousEpisodeHandler())
skill_builder.add_request_handler(NextEpisodeHandler())
skill_builder.add_request_handler(LoopOnEpisodeHandler())
skill_builder.add_request_handler(LoopOffEpisodeHandler())
skill_builder.add_request_handler(HelpHandler())
skill_builder.add_request_handler(FallbackHandler())

# Event Handlers
skill_builder.add_request_handler(PlaybackNearlyFinishedHandler())
skill_builder.add_request_handler(PlaybackFailedHandler())
skill_builder.add_request_handler(PlaybackStartedHandler())
skill_builder.add_request_handler(PlaybackStoppedHandler())
skill_builder.add_request_handler(PlaybackFinishedHandler())

# Request Interceptors
skill_builder.add_global_request_interceptor(RequestLogger())
skill_builder.add_global_request_interceptor(LoadStateRequestInterceptor())

# Response Interceptors
skill_builder.add_global_response_interceptor(ResponseLogger())
skill_builder.add_global_response_interceptor(SaveStateResponseInterceptor())

# Exception Handler
skill_builder.add_exception_handler(CatchAllExceptionHandler())

lambda_handler = skill_builder.lambda_handler()

A invocação dos intents é tratada no backend através de  RequestHandlers. Quando a skill é ativada com o comando de invocação, é enviada uma requisição para o backend com o tipo LaunchRequest. A boa prática é que, no handler encarregado por esta requisição, a skill responda ao usuário uma mensagem de boas-vindas, e instruções de utilização da skill.

class LaunchRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):

        player = Player(handler_input)
        player.reset()

        first_time = handler_input.attributes_manager.persistent_attributes.get("playback").get("firstTime")

        if first_time:
            speak_out = 'Olá, seja bem-vindo aos Drops da Exímia<emphasis level="strong">Cô</emphasis>. ' \
                        'Para começar você pode escolher o episódio mais recente dizendo: O mais recente, ' \
                        'ou ainda pesquisar sobre algum tema específico iniciando a frase com:  ' \
                        'O episódio sobre. <break time="500ms"/> ' \
                        'Para mais informações, basta me pedir Ajuda. ' \
                        '<break time="500ms"/>Qual episódio você gostaria de ouvir?'
        else:
            speak_out = 'Olá, seja bem-vindo de volta aos Drops da Exímia<emphasis level="strong">Cô</emphasis>. ' \
                        'Para qualquer dúvida, basta me pedir Ajuda. ' \
                        '<break time="500ms"/>Qual episódio você gostaria de ouvir?'

        handler_input.response_builder.speak(speak_out).ask(speak_out)

        return handler_input.response_builder.response

Outros elementos importantes existentes no ASK são os RequestInterceptors e os ResponseInterceptors.  Eles interceptam momentos antes e depois da execução dos handlers, respectivamente, possibilitando qualquer pré-processamento ou pós-processamento da requisição. Em nosso cenário, utilizamos estes interceptors para logging,  para carregar o estado persistente de execução do playback antes, e salvar as suas atualizações depois da invocação.

class SaveStateResponseInterceptor(AbstractResponseInterceptor):

    def process(self, handler_input, response):
        try:
            handler_input.attributes_manager.save_persistent_attributes()
        except:
            print("Ocorreu um erro ao persistir os dados da sessão")

Não menos importante, o ASK oferece um handler específico para capturar exceções não tratadas durante a execução da skill. É extremamente importante que a skill retorne sempre uma mensagem amigável ao usuário e que o problema seja tratado.

class CatchAllExceptionHandler(AbstractExceptionHandler):
    def can_handle(self, handler_input, exception):
        return True

    def handle(self, handler_input, exception):
        logger.error(exception, exc_info=True)
        notify_error(exception)

        speech = "Desculpe. Tive um problema para obter os dados do drops. Tente novamente mais tarde!"
        handler_input.response_builder.speak(speech)
        # .ask(speech)

        return handler_input.response_builder.response

Nos próximos posts da série, serão apresentadas as funções do player,  crawler, além do processo de publicação da skill. Para aqueles que gostariam de conhecer ou contribuir com o projeto, o código será disponibilizado publicamente no Github em breve. Por fim, para aqueles que não conhecem o nosso Drops,  ele está disponível nas principais plataformas de podcast do mercado, e agora também na Alexa!

Douglas Picolotto

Sou arquiteto de software, desenvolvedor, apaixonado por tecnologias, metodologias e padrões. Tenho mais de 15 anos de experiência, sempre em empresas que desenvolvem software de vanguarda. Sou apaixonado por Cloud Computing e sou especialista em tecnologias da Amazon.

Talvez você goste também

Carregando posts…

Mais posts da série Criando uma Skill para a Amazon Alexa

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *