DynamoDBの永続アトリビュートのあるAlexaスキルを作ってみた(Python)

2021.03.12

こんにちは、3/22にAlexaスキルビルダーが廃止になるという事で、アレクサを勉強することにしたリサリサです。

アレクサ道場を一通り見たので、とりあえず、簡単なスキルを作ってみました。アレクサのコードエディタを使ったのですが、DynamoDBにテーブルを作ったりする必要すらなく、本当にとても簡単に作れて感動したのでブログにしてみます!

作ったもの

毎月遊びに使うのは3万円と決めてはいるものの、家計のお財布と遊びのお財布を分けておらず、結局いくら使っているか分からない状態になっていたので、今月いくらお金を使えるのかを教えてくれるスキルを作ってみました。

DynamoDBに月のお小遣いと今月使える残高を持ち、それを更新していくだけのシンプルなスキルです。機能は以下の三つだけ。

起動時:今月の残りお小遣いを教えてくれる

設定:月に使えるお小遣いの金額を設定できる。残高リセット

お小遣い使用:お小遣いの使用を聞いて、残高を減らす

作ってみた

テンプレートを作成

モデルは「カスタムモデル」。バックエンドは「Alexa-hosted(Python)」にしました。

「スクラッチで作成」を選択し「テンプレートで続ける」。

1分ほど待つとテンプレートが完成します。以下のように、コードも既にたくさんかかれており、これを少しいじるだけで簡単なスキルは作れてしまいます。

そのままハローワールドを試すとこんな感じになります。

最初の○○を開いての○○はデフォルトはスキル名になっています。

インテントを書き換え

各機能ごとにをインテントというものを作ります。何を喋ったらその機能が動くのか、何をインプットで受け付けるのかを定義するものです。

今回の場合は、以下の2機能に対して作りました。

設定(SettingIntent):月に使えるお小遣いの金額を設定できる

お小遣い使用(PayIntent):お小遣いの使用を登録できる。残高を減らす

SettingIntent

「月のお小遣いは○○円」いう発話を受けると設定機能が動作します。○○円は数値で、この数値をpocketmoneyという変数(スロットといいます)で受け取るという設定をします。

PayIntent

「○○円使った」という発話を受けると設定機能が動作します。これもpayというスロットで受け取ります。

コードを書き換え

DynamoDBのアダプター追加

DynamoDBを使いたいので、以下の手順に沿って、それぞれコードを追加します。最後にコード全文を載せるので、手順の紹介は割愛します。

https://developer.amazon.com/ja-JP/docs/alexa/hosted-skills/alexa-hosted-skills-session-persistence.html

インテントに合わせてClassを追加

class SettingIntentHandler(AbstractRequestHandler):
	"""Handler for Setting Intent."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_intent_name("SettingIntent")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		pocketmoney = ask_utils.get_slot_value(handler_input=handler_input, slot_name='pocketmoney')

		attr = handler_input.attributes_manager.persistent_attributes
		attr['pocketmoney'] = pocketmoney
		attr['balance'] = pocketmoney

		handler_input.attributes_manager.session_attributes = attr
		handler_input.attributes_manager.save_persistent_attributes()

		speak_output = f"月のお小遣いを{attr['pocketmoney']}円に設定しました。今月はあと{attr['balance']}円のお小遣いを使えます。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				# .ask("add a reprompt if you want to keep the session open for the user to respond")
				.response
		)

class PayIntentHandler(AbstractRequestHandler):
	"""Handler for Pay Intent."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_intent_name("PayIntent")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		pay = ask_utils.get_slot_value(handler_input=handler_input, slot_name="pay")

		attr = handler_input.attributes_manager.persistent_attributes
		attr['balance'] = str(int(attr['balance']) - int(pay))

		handler_input.attributes_manager.session_attributes = attr
		handler_input.attributes_manager.save_persistent_attributes()
		
		speak_output = f"{pay}円使ったので、今月使える残りのお小遣いは{attr['balance']}円です。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				# .ask("add a reprompt if you want to keep the session open for the user to respond")
				.response
		)

追加したClassをSkillBuilderに追加

sb = CustomSkillBuilder(persistence_adapter = dynamodb_adapter)

sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(SettingIntentHandler())
sb.add_request_handler(PayIntentHandler())
sb.add_request_handler(HelpIntentHandler())
sb.add_request_handler(CancelOrStopIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())
sb.add_request_handler(IntentReflectorHandler()) # make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers

sb.add_exception_handler(CatchAllExceptionHandler())

起動時の処理を書き換え

class LaunchRequestHandler(AbstractRequestHandler):
	"""Handler for Skill Launch."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool

		return ask_utils.is_request_type("LaunchRequest")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response

		attr = handler_input.attributes_manager.persistent_attributes
		if not attr:
			attr['pocketmoney'] = '30000'
			attr['balance'] = '30000'

		handler_input.attributes_manager.session_attributes = attr
		handler_input.attributes_manager.save_persistent_attributes()

		speak_output = f"あなたの今月の残りお小遣いは{attr['balance']}円です。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				.ask(speak_output)
				.response
		)

動かしてみた

100円使ってみる。

お小遣いの設定変えてみる

終了して開きなおしてみる。

できたーー!

せっかくなので、審査にも出してみましたw

追記

すごい簡易的な履歴機能(過去にさかのぼったりできない…)つけて、公開できました!

全文

Pythonが公開されて1年半。サンプルコードがあまり落ちていなくて困ったので、ほぼテンプレートのままですが全文を貼っておきます。どなたかのお役に立てば幸いです。

reauirements.txt

boto3==1.9.216
ask-sdk-core==1.11.0
ask-sdk-dynamodb-persistence-adapter==1.15.0

lambda_function.py

# -*- coding: utf-8 -*-

# This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK for Python.
# Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
# session persistence, api calls, and more.
# This sample is built using the handler classes approach in skill builder.
import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder,CustomSkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

import os
import boto3
from ask_sdk_dynamodb.adapter import DynamoDbAdapter

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

class LaunchRequestHandler(AbstractRequestHandler):
	"""Handler for Skill Launch."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool

		return ask_utils.is_request_type("LaunchRequest")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response

		attr = handler_input.attributes_manager.persistent_attributes
		if not attr:
			attr['pocketmoney'] = '30000'
			attr['balance'] = '30000'

		handler_input.attributes_manager.session_attributes = attr
		handler_input.attributes_manager.save_persistent_attributes()

		speak_output = f"あなたの今月の残りお小遣いは{attr['balance']}円です。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				.ask(speak_output)
				.response
		)


class SettingIntentHandler(AbstractRequestHandler):
	"""Handler for Setting Intent."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_intent_name("SettingIntent")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		pocketmoney = ask_utils.get_slot_value(handler_input=handler_input, slot_name='pocketmoney')

		attr = handler_input.attributes_manager.persistent_attributes
		attr['pocketmoney'] = pocketmoney
		attr['balance'] = pocketmoney

		handler_input.attributes_manager.session_attributes = attr
		handler_input.attributes_manager.save_persistent_attributes()

		speak_output = f"月のお小遣いを{attr['pocketmoney']}円に設定しました。今月はあと{attr['balance']}円のお小遣いを使えます。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				# .ask("add a reprompt if you want to keep the session open for the user to respond")
				.response
		)

class PayIntentHandler(AbstractRequestHandler):
	"""Handler for Pay Intent."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_intent_name("PayIntent")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		pay = ask_utils.get_slot_value(handler_input=handler_input, slot_name="pay")

		attr = handler_input.attributes_manager.persistent_attributes
		attr['balance'] = str(int(attr['balance']) - int(pay))

		handler_input.attributes_manager.session_attributes = attr
		handler_input.attributes_manager.save_persistent_attributes()
		
		speak_output = f"{pay}円使ったので、今月使える残りのお小遣いは{attr['balance']}円です。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				# .ask("add a reprompt if you want to keep the session open for the user to respond")
				.response
		)


class HelpIntentHandler(AbstractRequestHandler):
	"""Handler for Help Intent."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_intent_name("AMAZON.HelpIntent")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		speak_output = "あなたの今月の残りのお小遣いを教えます。月のお小遣いはいくら。と言って頂ければそれを毎月の使用可能額に設定します。いくら使ったと言って頂ければそれを差し引きます。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				.ask(speak_output)
				.response
		)


class CancelOrStopIntentHandler(AbstractRequestHandler):
	"""Single handler for Cancel and Stop Intent."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return (ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input) or
				ask_utils.is_intent_name("AMAZON.StopIntent")(handler_input))

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		speak_output = "Goodbye!"

		return (
			handler_input.response_builder
				.speak(speak_output)
				.response
		)


class SessionEndedRequestHandler(AbstractRequestHandler):
	"""Handler for Session End."""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_request_type("SessionEndedRequest")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response

		# Any cleanup logic goes here.

		return handler_input.response_builder.response


class IntentReflectorHandler(AbstractRequestHandler):
	"""The intent reflector is used for interaction model testing and debugging.
	It will simply repeat the intent the user said. You can create custom handlers
	for your intents by defining them above, then also adding them to the request
	handler chain below.
	"""
	def can_handle(self, handler_input):
		# type: (HandlerInput) -> bool
		return ask_utils.is_request_type("IntentRequest")(handler_input)

	def handle(self, handler_input):
		# type: (HandlerInput) -> Response
		intent_name = ask_utils.get_intent_name(handler_input)
		speak_output = "You just triggered " + intent_name + "."

		return (
			handler_input.response_builder
				.speak(speak_output)
				# .ask("add a reprompt if you want to keep the session open for the user to respond")
				.response
		)


class CatchAllExceptionHandler(AbstractExceptionHandler):
	"""Generic error handling to capture any syntax or routing errors. If you receive an error
	stating the request handler chain is not found, you have not implemented a handler for
	the intent being invoked or included it in the skill builder below.
	"""
	def can_handle(self, handler_input, exception):
		# type: (HandlerInput, Exception) -> bool
		return True

	def handle(self, handler_input, exception):
		# type: (HandlerInput, Exception) -> Response
		logger.error(exception, exc_info=True)

		speak_output = f"もう一度お願いします。"

		return (
			handler_input.response_builder
				.speak(speak_output)
				.ask(speak_output)
				.response
		)

# The SkillBuilder object acts as the entry point for your skill, routing all request and response
# payloads to the handlers above. Make sure any new handlers or interceptors you've
# defined are included below. The order matters - they're processed top to bottom.

ddb_region = os.environ.get('DYNAMODB_PERSISTENCE_REGION')
ddb_table_name = os.environ.get('DYNAMODB_PERSISTENCE_TABLE_NAME')

ddb_resource = boto3.resource('dynamodb', region_name=ddb_region)
dynamodb_adapter = DynamoDbAdapter(table_name=ddb_table_name, create_table=False, dynamodb_resource=ddb_resource)

sb = CustomSkillBuilder(persistence_adapter = dynamodb_adapter)

sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(SettingIntentHandler())
sb.add_request_handler(PayIntentHandler())
sb.add_request_handler(HelpIntentHandler())
sb.add_request_handler(CancelOrStopIntentHandler())
sb.add_request_handler(SessionEndedRequestHandler())
sb.add_request_handler(IntentReflectorHandler()) # make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers

sb.add_exception_handler(CatchAllExceptionHandler())

lambda_handler = sb.lambda_handler()