naritoブログ

【お知らせ】
・コメントで質問等をしたが返事が返ってこない場合、私はそれを見落としています。
その場合は再度コメントをするかメールをしてください(toritoritorina@gmail.com)。

・近いうちに新しいブログが作成されます。わーお!

Alexa Skills Kitで、シンプルなクイズを作成する(Python)

約205日前 2018年3月2日17:14
プログラミング関連
Python Alexa Skills Kit
ASKで、シンプルなクイズをPythonで実装しました。
以下のような流れで進みます。

あなた: アレクサ、なりとの映画クイズを開いて
アレクサ: なりとの映画クイズへようこそ。映画に関するクイズを5つ出題しますので、答えを「1番」のように番号で答えてください....それでは最初の問題です。次のうち....
あなた: 1番
アレクサ: 2問目です。.....
あなた: もう一回喋って
アレクサ: 2問目です。....
あなた: 4番
(中略)
アレクサ: お疲れ様でした。5問中、3問正解です。また遊んでください。


対話モデルは以下のようになりました。スキルビルダーのCode Editor欄の内容です。
{
  "languageModel": {
    "intents": [
      {
        "name": "AMAZON.CancelIntent",
        "samples": []
      },
      {
        "name": "AMAZON.HelpIntent",
        "samples": []
      },
      {
        "name": "AMAZON.RepeatIntent",
        "samples": [
          "もう一回言って",
          "もう一回",
          "もっかい言って",
          "もう一回喋って",
          "もう一回話して",
          "もう一回教えて"
        ]
      },
      {
        "name": "AMAZON.StopIntent",
        "samples": []
      },
      {
        "name": "QuestionIntent",
        "samples": [
          " {Answer} 番",
          " {Answer} 番です",
          " {Answer} 番かな",
          " {Answer} 番だよ",
          " {Answer} 番だぜ"
        ],
        "slots": [
          {
            "name": "Answer",
            "type": "AMAZON.NUMBER"
          }
        ]
      }
    ],
    "invocationName": "なりとの映画クイズ"
  }
}



以下は組み込みのインテントです。そのまま使えるのですが、追加で対話サンプルを設定することもできます。今回はRepeatIntentにいくつかの対話サンプルを増やし、もう一回喋ってほしい場合に上手く動作するようにしました。
AMAZON.CancelIntent
AMAZON.HelpIntent
AMAZON.RepeatIntent
AMAZON.StopIntent



QuestionIntentは自分で作成したもので、「1番です」のような回答と紐付けています。対話サンプルに{ Answer } のように書いていますが、これは回答が「1番です」ならば1の部分を取り出し、Answerという名前に紐付けて保存がされます。type: Amazon.NUMBERは、「ご」を「5」、「いち」を「1」のように変換してくれます。
        "name": "QuestionIntent",
        "samples": [
          " {Answer} 番",
          " {Answer} 番です",
          " {Answer} 番かな",
          " {Answer} 番だよ",
          " {Answer} 番だぜ"
        ],
        "slots": [
          {
            "name": "Answer",
            "type": "AMAZON.NUMBER"
          }
        ]
      }


今のところは全部で5問です。5問ちゃんと相手をすると、出題と回答がカードで表示されます。
カードの様子


今回はAWS Lambdaを使いましたが、そのソースコードは以下です。
DESCRIPTION = """
なりとの映画クイズへようこそ。映画に関するクイズを5つ出題しますので、答えを「1番」のように番号で答えてください。
聞き取れなかった場合は「もう一回喋って」。
クイズを終わりたい場合は「キャンセル」。
このガイドを説明してほしい際は、「ヘルプ」。
と話してください。
"""

Q1 = """
パルプ・フィクションでアカデミー賞助演男優賞にノミネートされ、「アベンジャーズ」「ヘイトフルエイト」「キングコング:髑髏島の巨神」などにも出演。
史上最高の興行収入を上げた俳優としてギネス記録も達成した方です。次のうち何番でしょうか。
1番:モーガン・フリーマン。
2番:サミュエル・ジャクソン。
3番:ウィル・スミス。
"""

Q2 = """
また、モフモフしようぜ!!のポスターでおなじみで、中年テディベアと、その親友が主役のこの作品、次のうち何番でしょうか。
1番:マスク。
2番:テッド。
3番:ベイブ
"""

Q3 = """
みんな大好きアメコミヒーローに関しての出題です。次の中に仲間外れのヒーローがいます。何番でしょうか。
1番:アイアンマン。
2番:ハルク。
3番:キャプテン・アメリカ。
4番:スーパーマン。
"""

Q4 = """
脱走や脱獄をテーマにした映画は沢山あります。次の中で、脱走・脱獄とは無関係な映画は何番でしょうか。
1番:エイトマイル。
2番:ショーシャンクの空に。
3番:大脱走。
4番:アルカトラズからの脱出
"""

Q5 = """
「アイル・ビー・バック」という有名なセリフが出てくる映画のシリーズがあります。次のうち、どのシリーズでしょうか。
1番:エイリアン。
2番:バイオハザード。
3番:インディ・ジョーンズ。
4番:ターミネーター
"""

questions = {
    1: (Q1, 2),
    2: (Q2, 2),
    3: (Q3, 4),
    4: (Q4, 1),
    5: (Q5, 4),
}


def lambda_log(debug_mode=False):
    """lambda_handlera関数実行時にログを取るデコレータ

    スキル実行時に、何らかのエラーが出た際は
    このデコレータによってリクエストの内容が出力されます。
    debug_mode引数をTrueにすると、全てのリクエストの内容が出力されます。
    """
    def _debug(function):
        def __debug(event, context):
            try:
                response = function(event, context)
            except Exception as e:
                # エラー時は、必ずリクエストを表示し、エラーで終了させる
                print(event)
                raise
            else:
                # デバッグモードTrue時は、全てのリクエストとレスポンスを出力
                if debug_mode:
                    print(event, response)
                return response
        return __debug
    return _debug


class BaseSpeech:
    """シンプルな、発話するレスポンスのベース"""

    def __init__(self, speech_text, should_end_session, session_attributes=None):
        """初期化処理

        引数:
            speech_text: Alexaに喋らせたいテキスト
            should_end_session: このやり取りでスキルを終了させる場合はTrue, 続けるならFalse
            session_attributes: 引き継ぎたいデータが入った辞書
        """
        if session_attributes is None:
            session_attributes = {}

        # 最終的に返却するレスポンス内容。これを各メソッドで上書き・修正していく
        self._response = {
            'version': '1.0',
            'sessionAttributes': session_attributes,
            'response': {
                'outputSpeech': {
                    'type': 'PlainText',
                    'text': speech_text
                },
                'shouldEndSession': should_end_session,
            },
        }

        # 取り出しやすいよう、インスタンスの属性に
        self.speech_text = speech_text
        self.should_end_session = should_end_session
        self.session_attributes = session_attributes

    def simple_card(self, title, text=None):
        """シンプルなカードを追加する"""
        if text is None:
            text = self.speech_text
        card = {
            'type': 'Simple',
            'title': title,
            'content': text,
        }
        self._response['response']['card'] = card
        return self

    def build(self):
        """最後にこのメソッドを呼んでください..."""
        return self._response


class OneSpeech(BaseSpeech):
    """1度だけ発話する(ユーザーの返事は待たず、スキル終了)"""

    def __init__(self, speech_text, session_attributes=None):
        super().__init__(speech_text, True, session_attributes)


class QuestionSpeech(BaseSpeech):
    """発話し、ユーザーの返事を待つ"""

    def __init__(self, speech_text, session_attributes=None):
        super().__init__(speech_text, False, session_attributes)

    def reprompt(self, text):
        """リプロンプトを追加する"""
        reprompt = {
            'outputSpeech': {
                'type': 'PlainText',
                'text': text
            }
        }
        self._response['response']['reprompt'] = reprompt
        return self


def welcome(session):
    """最初の出題。説明の後に、1問目を出題"""
    message = DESCRIPTION + 'それでは、最初の問題です。' + Q1
    return QuestionSpeech(
        message,
        session_attributes=session
    ).reprompt('よく聞こえませんでした').build()


def quize(session):
    """クイズの出題"""
    current = session['number']
    question = questions[current][0]
    message = '{0}問目です。'.format(current)
    message += question
    return QuestionSpeech(
        message,
        session_attributes=session
    ).reprompt('よく聞こえませんでした').build()


def exit(session):
    """クイズの終了"""
    message = 'お疲れ様でした。{0}問中、{1}問正解です。また遊んでください。'.format(
        session['number']-1, session['correct']
    )
    # 5問全部やってくれたら、問題と答えをカードで表示させる
    if session['number']-1 == len(questions):
        card_text = message + '\n'
        for question, answer in questions.values():
            card_text += question + '\n'
            card_text += '正解:{0}'.format(answer)
            card_text += '\n\n'
        return OneSpeech(message).simple_card('出題された問題一覧', text=card_text).build()

    # 途中でキャンセルしたら、単純に終わり
    else:
        return OneSpeech(message).build()


def redoing(session):
    """意図しない答え(あいうえお、等)"""
    message = '聞き取れませんでした。答えを「1番」のように番号で答えてください'
    return QuestionSpeech(
        message,
        session_attributes=session
    ).reprompt('よく聞こえませんでした').build()


def guido(session):
    """ユーザーガイドの説明"""
    return QuestionSpeech(
        DESCRIPTION,
        session_attributes=session
    ).reprompt('よく聞こえませんでした').build()


def judge(session, slots):
    """答えが合っているかを確認する"""
    current = session['number']
    user_answer = int(slots['Answer']['value'])  # 文字列で取得される
    question_answer = questions[current][1]

    # 答えがあっていれば、correctに+1
    if user_answer == question_answer:
        session['correct'] += 1

    # 次の問題に進むために、numberを+1
    session['number'] += 1


@lambda_log(debug_mode=False)
def lambda_handler(event, context):
    """最初に呼び出される関数"""
    # リクエストの種類を取得
    request = event['request']
    request_type = request['type']

    # 起動時は新しい辞書を作成し、そうでなければ取り出す
    # 現在の問題(何番目)と正解数がキーの辞書
    if request_type == 'LaunchRequest' or event['session']['new']:
        session = {'number': 1, 'correct': 0}
        return welcome(session)  # 初回起動時は必ずウェルカムメッセージ
    elif request_type == 'IntentRequest':
        session = event['session']['attributes']
        intent_name = request['intent']['name']
        if intent_name == 'QuestionIntent':
            slots = request['intent']['slots']
            # exceptには、「あいうえお」等の意図しない答えの場合に入ります。
            try:
                judge(session, slots)  # 前回の答え合わせと、次の問題を取得するための準備処理
            except (ValueError, KeyError):
                return redoing(session)
            else:
                # n番!と答えて、クイズが全て終わればexit関数、次があればquize関数
                if session['number'] > len(questions):
                    return exit(session)
                else:
                    return quize(session)

        # 「もう一回喋って」等で呼ばれる、組み込みインテント
        elif intent_name == 'AMAZON.RepeatIntent':
            return quize(session)

        # 「キャンセル」「取り消し」「やっぱりやめる」等で呼び出される。組み込みのインテント
        elif intent_name == 'AMAZON.CancelIntent' or intent_name == 'AMAZON.StopIntent':
            return exit(session)

        # 「ヘルプ」で呼び出される
        elif intent_name == 'AMAZON.HelpIntent':
            return guido(session)





BaseSpeech、OneSpeech、QuestionSpeechは入門シリーズでも使いました、ちょっとした便利クラスです。
lambda_handlerについているデコレータですが、これはlambda_handler内で何か例外が上がれば、リクエストの内容とエラーを出力するデコレータです。debug_modeをTrueにすると全てのリクエストが出力されるようになります。
def lambda_log(debug_mode=False):
    """lambda_handlera関数実行時にログを取るデコレータ

    スキル実行時に、何らかのエラーが出た際は
    このデコレータによってリクエストの内容が出力されます。
    debug_mode引数をTrueにすると、全てのリクエストの内容が出力されます。
    """
    def _debug(function):
        def __debug(event, context):
            try:
                response = function(event, context)
            except Exception as e:
                # エラー時は、必ずリクエストを表示し、エラーで終了させる
                print(event)
                raise
            else:
                # デバッグモードTrue時は、全てのリクエストとレスポンスを出力
                if debug_mode:
                    print(event, response)
                return response
        return __debug
    return _debug
...
...
@lambda_log(debug_mode=False)
def lambda_handler(event, context):



では、lambda_handler関数内から見ていきます。
○○を開いて!のときは 'LaunchRequest'ですが、○○を開いて1番!のようにインテントを含むと'IntentRequest'となります。どのように起動されても、最初はメッセージを表示させるため、if request_type == 'LaunchRequest' or event['session']['new']: としています。if event['session']['new':だけでも充分ですが、この方が見た目的にも意味が通るでしょう。
    # リクエストの種類を取得
    request = event['request']
    request_type = request['type']

    # 起動時は新しい辞書を作成し、そうでなければ取り出す
    # 現在の問題(何番目)と正解数がキーの辞書
    if request_type == 'LaunchRequest' or event['session']['new']:
        session = {'number': 1, 'correct': 0}
        return welcome(session)  # 初回起動時は必ずウェルカムメッセージ



今回のクイズでは、現在クイズの何問目か、いくつ正解したか、といった情報を保存しておく必要がありますが、これはセッションで管理します。Webのセッションとイメージは同じです。スキルを終了するまで、サーバーとクライアントの間でデータを保存・共有させることができます。
session = {'number': 1, 'correct': 0}



この辞書は、最終的には以下のようなJSON(辞書)になります。sessionAttributesの部分ですね。
'version': '1.0',
'response': {
    'outputSpeech': {
        'type': 'PlainText',
        'text': 'ハロー'
    },
    'shouldEndSession': False,
},
'sessionAttributes': {
    'number': 1,
    'correct': 0
}



初回起動時でなければ、セッションは既に渡されていますので、それをevent辞書から取得する必要があります。
    elif request_type == 'IntentRequest':
        session = event['session']['attributes']



リクエストには以下のような感じで含まれています。これも簡単に取り出せますね。
"session": {
...
...
    "attributes": {'number': 2, 'correct': 0},
},



今回は「1番」であれば、その「1」が取り出せると先に書きました。このデータがどこに含まれているかというと、リクエストのintent→slotsになります。
"request": {
...
...
"intent": {
  "name": "Greet",
  'slots': {
    'Answer': {'name': 'Answer', 'value': '4', 'confirmationStatus': 'NONE'}
 },


これを取り出しているのコードが、関数内の以下の部分です。
    elif request_type == 'IntentRequest':
        session = event['session']['attributes']
        intent_name = request['intent']['name']
        if intent_name == 'QuestionIntent':
            slots = request['intent']['slots']  # ここです



取り出す際はslots['Answer']['value']とする必要があるのですが、いくつか注意があります。「ご」という発音を「'5'」に変換するところまでは自動でしてくれますが、値は文字列です。なので、数値に変換するならばint関数を使います。
slots['Answer']['value']
int(slots['Answer']['value'])



「あいうえお番」のように答えると不思議な値が入ることもありますし、QuestionIntentと判断されたが、{ Answer }に当たる部分が見つからない場合はvalueキーがありません。
今回は、数値に変換できない文字列をint関数に渡した際のValueError、valueキーがない場合のKeyErrorを捕まえることで、意図しない答えだった場合を判別しています。
            try:
                judge(session, slots)  # 前回の答え合わせと、次の問題を取得するための準備関数。2種類のエラーがでるかも
            except (ValueError, KeyError):
                return redoing(session)  # 意図しない答えだった場合は、「もっかい答えて」と発話させる
            else:



残りの処理は割と単純です。興味があれば見てみてください。
くりかえしクイズ 約112日前 2018年6月3日17:19 返信する
例えば50問作って、このシンプルなクイズでPython使用した場合、
次使用した時も、同じ順番で50問とかなければ、最後の問題がでない。
"attributes": {'number': 2, 'correct': 0},は、前から後ろまで順番に解いていくクイズですね。

50問からランダムに10問選ぶ場合、同じ問題が選択されないよう
event['session']['attributes']に記憶する方法てあるのでしょうか。
なりと 約112日前 2018年6月3日23:01
"attributes": {'number': 2, 'correct': 0, 'log': [1, 3, 5]},
のように、既に出された問題をセッションに保存しておき、次の問題を取り出すときはそれを除くようにするとどうでしょう。
くりかえしクイズ 約111日前 2018年6月4日21:15 返信する
回答有難う、もう一つ、pythonでは普通のスロット値は
sl=slots['Answer']['value'] で取り出せる。

同義語スロット値のIDを取り出そうとして
Avalues=intent['slots']['Answer']['resolutions']['resolutionsPerAuthority']
valusesの文字列は取り出せたが
idはエラーで取り出せない。なんか基本が間違えてるかもしれない。
教えてください。
なりと 約110日前 2018年6月5日18:24
idは、values[0]['value']['id']のようにして取得できませんか。
https://developer.amazon.com/ja/docs/custom-skills/define-synonyms-and-ids-for-slot-type-values-entity-resolution.html
くりかえしクイズ 約110日前 2018年6月5日20:44 返信する
Resolutionsオブジェクトに
resolutionsPerAuthority[].values[].value.id
と書かれているので
values=intent['slots']['Answer']['resolutions']['resolutionsPerAuthority'][0]['values']
id=values[0]['value']['id']
でidが表示されました。お手数かけ、有難う。