見出し画像

【EP1-5】コーヒーショップの作成

皆さんコーヒー好きですか?私は欠かさず毎日飲んでいます。
心が落ち着きますよね。

Alexa道場にてコーヒーショップスキルを作成します。Alexa道場のSeason2がコーヒーショップスキルの作成となります。
Amazon公式のスキル開発動画を観て学習します。

Season 2
 何を学習するかアジェンダとなります。

Season 2 EP1
 Amazonマーケットでのスキルについて説明です。

Season 2 EP2
 Alexaとサーバサイドリクエストのやり取りを図式説明しています。
 Jsonによる電文でリクエストが送られていることを学習できます。

Season 2 EP3
 Alexaのスキルの種類について説明です。スキルを最初に開発する時にどの 
 モデルで作成するのかを最初に選択します。
  最初に作る場合はカスタムでいいです。
 そういう種類のスキルがあるんだぁぐらいの学習でいいです。

Season 2 EP4
 対話の仕方を学べます。一回一回のやり取りしかできない事が
 理解できます。

Season 2 EP5
 実際の作り方はここからなので、概要を理解したらここからスキル作成を
 スタートさせましょう。
 スキル名つけるけど同じスキル名で重複して問題ないの?とか最初
 気になりましたが問題ありません。
 ここで作成したスキル名は公開されていない個人のスキルですので
 スキル名が何かと重複してもまだ世界に公開されていないので
 問題ありません。本当に公開する時は呼び出し名の重複は出来ないので
 捻って考える必要があります。

Season 2 EP6
 インテントなどここでは言葉の定義を覚えます。この後の話に
 ついていけるようにする必要があるので、ここで言葉の定義を
 覚えましょう。
 インテントとは何をいったら何のAlexa反応を呼び出すかを
 決めるものになります。

Season 2 EP7
 HelloWorldスキルをベースにカスタマイズしていきます。
 実作業として見ながらやるのはここからです。
 とりあえず同じように真似をしてやってみてください。

Season 2 EP8
 このエピソードでは、Alexaスキルの「テスト」が学べます。
  Alexaデバイスを持っていなくてもテストが出来ます。やってみると
 分かりますがデバイスと同じです。
 リクエストされた内容がどのようにJSONで来ているのか分かります。
  リクエストから値を取得する場合にとても重要なので構文の見方について
 理解してください。


ザーッとS02 E08までAlexaスキルの特性などを学習しました。

色々見て学ぶより書いて学ぶのが一番です!

「コーヒーショップ」というサンプルスキルを作ります。
Alexaにコーヒーなどの飲み物を頼んで受け答えをするというものです。
このセッションの良いところは序盤はものすごく簡単に作れるのですが、
ベースで作ったスキルをどんどんカスタマイズしていき、立派なコーヒーショップに育てていくところです。

画像1

Alexaは女の子の声なのでこんな感じのコーヒーショップを想像しながら作りましょう。

Let's Make Skill!
さぁ、上述した動画と共に、スキル開発トレーニングページも見ながら作成します。


https://developer.amazon.com/ja/blogs/alexa/post/93bad9f6-377f-4ada-887c-279e7346f729/alexatraining-customskill

何回かに分かれて作られているので、動画に合わせて確認する部分を見ていく形です。


はい、見ていただけたでしょうか?
そうです。 node.js で書かれています。
私は巷で人気が出てきたpythonで書きたいんです。でもサンプルコードがnode.jsです。。。

まず壁に当たるのが、node.jsで規定しているクラスがpythonではそのままではないんです。

例えばセッションアトリビュート
〇for node.js
handlerInput.attributesManager.getSessionAttributes();

〇for python
handler_input.attributes_manager.session_attributes

これがかなりの苦戦です。何が苦戦ってサンプルになっている「attributesManager」の記述をネットで「attributesManager python」とやっても完璧なヒットはしないんです。

毎回「書き方分かんねぇなぁ(;´∀`)」という形で探してました。

そんな自分がたどり着いたのが「Alexa Skills Kit SDK for Python」のドキュメントです。
https://alexa-skills-kit-python-sdk.readthedocs.io/ja/latest/

ここにはpythonの書き方が親切にあります。
ただここ、google検索でピンポイントで探せないんです。検索させないようにしているんですかね?

ここでは様々な書き方について切り替えて確認することができます。
https://developer.amazon.com/ja-JP/docs/alexa/alexa-presentation-language/use-apl-with-ask-sdk.html

・node.jsの切り替え

画像2

・pythonの切り替え

画像3

分かります?このタブの切り替えによってコードが変わります。
なのでサンプルでよく出ているnode.jsをpythonの書き方で見て学んでいきます。


でもね、やっぱりそう簡単には行かないのよね

そう、うまくいかないんです。このコーヒーショップを作るのも実際書いてデバッグして書いてデバッグしてと子供も構わないでずっとやってました(笑)

ってことで、ここで挫折してしまう人がいるのが非常にもったいないので私のコーヒーショップスキル(確か最後まで完成していたはず?)を載せておきます。
挫折しないで続けていきましょう。ここを乗り切ると
「俺、作れるんじゃね?」という妙な自信が湧いてきますので、
見せるんですが、出来ることなら悩んで答え合わせのような形で見てもらいたいです。努力した後に見えてくる景色を見てください。

【インテント】
 〇OrderIntent

#これはファイルにして取り込んだ方が早い

いつもので
いつもの
前回と同じの
じゃあそれで
{amount}
{amount} つ
{amount} 杯
{amount} {unit} 下さい
{amount} {unit} お願い
{menu}
{menu} をお願いします
{menu} を {amount} {unit} お願いします
{menu} を {amount} {unit} 欲しい
{menu} を {amount} {unit} がいいな
{menu} を {amount} {unit}
{menu} を {amount} {unit} 注文して

 〇インテントスロット

#これは一個一個手書きで作るしかない

スロット名 : スロットタイプ

menu  : menu_list
amount : AMAZON.NUMBER
unit  : unit_type

 〇スロットタイプ menu_list

#これはCSVファイルにして取り込んだ方が楽

エスプレッソ,004
カプチーノ,003,カップチーノ
カフェラテ,002,コーヒー牛乳,カフェ・オ・レ,カフェオレ
ホットコーヒー,001,あったかいコーヒー,温かいコーヒー,あたたかいコーヒー,あったかいコーヒー,ブレンド,コーヒー

 〇スロットタイプ unit_type

#これはCSVファイルにして取り込んだ方が楽

個,
つ,
杯,


【コード】

 〇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

# S3 アダプタ インポート START
import os
import boto3
from ask_sdk_s3.adapter import S3Adapter
# CustomSkillBuilderを使用しなければならない
from ask_sdk_core.skill_builder import CustomSkillBuilder
# S3 アダプタ インポート END

#2. initialize the persistence adapter to your custom skill builder BUKET名を設定する
s3_adapter = S3Adapter(bucket_name=os.environ["S3_PERSISTENCE_BUCKET"])

#from ask_sdk_core.skill_builder import SkillBuilder
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

# CustomSkillBuilderを使用しなければならない
sb = CustomSkillBuilder(persistence_adapter=s3_adapter)

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):
       
       # 前回データ読込
       last_order = handler_input.attributes_manager.persistent_attributes
       if not last_order:
           last_order['menu'] = "なし"
           last_order['amount'] = "0"
       
       handler_input.attributes_manager.session_attributes = last_order
       #永続アトリビュートに保存
       handler_input.attributes_manager.save_persistent_attributes()

       logger.info("last order is : %s",last_order)
       logger.info("session_attributes is : %s",handler_input.attributes_manager.session_attributes)
       logger.info("persistent_attributes is : %s",handler_input.attributes_manager.persistent_attributes)
       
       menu = last_order['menu']
       amount = last_order['amount']
       
       speak_output = "コーヒーショップへようこそ。前回は" + menu + "を" + amount + "つでしたね。今日は何にしますか?"
       reprompt_output = "今日は何にしますか?"

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


class OrderIntentHandler(AbstractRequestHandler):
   """Handler for Hello World Intent."""
   def can_handle(self, handler_input):
       # type: (HandlerInput) -> bool
       return ask_utils.is_intent_name("OrderIntent")(handler_input)

   def handle(self, handler_input):
       # type: (HandlerInput) -> Response
       # 最初に念の為いれておく
       drink_number = "つ"

       attributes = handler_input.attributes_manager.session_attributes
       
       #メニューのslot受け取り slotにない場合はセッションアトリビュートから取り出す 
       menu = handler_input.request_envelope.request.intent.slots["menu"].value or attributes.get("menu", None)

       #数のslot受け取り slotにない場合はセッションアトリビュートから取り出す 
       amount = handler_input.request_envelope.request.intent.slots["amount"].value or attributes.get("amount", None)

       #シノニムにマッチしたかを確認する
       #if(handler_input.request_envelope.request.intent.slots["menu"].resolutions.resolutions_per_authority[0].status.code == "ER_SUCCESS_MATCH"):
       #if handler_input.request_envelope.request.intent.slots["menu"].resolutions.resolutions_per_authority[0].status.code == StatusCode.ER_SUCCESS_MATCH:
           # マッチした場合は強制的に型で定義した名前に変更する
       #    menu = handler_input.request_envelope.request.intent.slots["menu"].resolutions.resolutions_per_authority[0].values[0].value.name

       # メニューが設定されていない場合
       if menu is None:
           logger.info("menu is none")
           attributes['amount'] = amount
           handler_input.attributes_manager.session_attributes = attributes
           speak_output = "何を注文しますか?"
           reprompt_output = "何を注文しますか?"
           
           return (
               handler_input.response_builder
                   .speak(speak_output)
                   .ask(reprompt_output)
                   .response
           )         

       # 数が設定されていない場合
       if amount is None:
           logger.info("amount is none")
           attributes['menu'] = menu
           handler_input.attributes_manager.session_attributes = attributes
           speak_output = "おいくつ注文しますか?"
           reprompt_output = "おいくつ注文しますか?"
           
           return (
               handler_input.response_builder
                   .speak(speak_output)
                   .ask(reprompt_output)
                   .response
           )   

       # 返答
       speak_output = menu + "を" + amount + drink_number + "ですね。ありがとうございます。"
       
       last_order = {
           "menu": menu,
           "amount": amount
       }
       
       #永続アトリビュートに保存
       handler_input.attributes_manager.persistent_attributes = last_order
       handler_input.attributes_manager.save_persistent_attributes()
       
       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 = "コーヒーショップへようこそ。今日は何にしますか?"
       reprompt_output = "今日は何にしますか?"

       return (
           handler_input.response_builder
               .speak(speak_output)
               .ask(reprompt_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 = "ご来店ありがとうございます。またのお越しをお待ちしております。"

       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 = 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 = "すみません。何だか上手くいかないようです。もう一度お試しください。"

       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.

sb.add_request_handler(LaunchRequestHandler())
sb.add_request_handler(OrderIntentHandler())
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()

 〇requirements.txt 外部ライブラリの取り込み

boto3==1.9.216
ask-sdk-core==1.11.0
ask-sdk-s3-persistence-adapter==1.0.0
ask-sdk-core==1.11.0
boto3==1.9.216

参考になりましたでしょうか?いろいろなサイトやGithubに落ちているコードを読み漁ってやっと作り切ったのを覚えています。
頑張って挫折しないで続けていきましょう!この先にスキル開発者としての未来が待っています。

余談ですが、寝る前に子供たちがこのスキルを呼び出し
「ヤクルトを100個!」など注文してケラケラ笑っています。

自分のスキルを呼び出して遊んでくれているっというのは、一番のユーザ(宝)ですね。




この記事が気に入ったらサポートをしてみませんか?