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!