日記BOTに質問機能を追加する

お疲れさまです、みやもとです。

LINEBOTに関してはいったん終わりと書きましたがもうひとつだけ。
質問機能をつけようと思います。

## 見慣れない単語が多い
日記を推理小説風の英文にしてクイズを付けて、わりと楽しく使っているのですがひとつ問題がありました。
使われている単語に見慣れないものが多い。

もともと私が英語に弱いところも大きいのですが、文学的表現のせいなのかちょっと見慣れない単語が出てくるとそこで目が留まってしまうのです。
クイズは３択なので最悪あてずっぽうでも答えられますが、せっかく作ったからにはもうちょっとちゃんと読んで答えられるようにしたい。
なのでクイズ挑戦前に質問できる機能を付けることにしました。

## コード変更
今回は変更箇所だけ書いていこうと思います。
変更前および変更していない箇所については前回記事のコードをご参照ください。

### import部分

```diff_python:main.py
from flask import abort, jsonify
import os
import base64, hashlib, hmac
import google.generativeai as genai
import json
from datetime import datetime, date
import pytz
+ import requests
from typing import Optional
```
しょっぱなからですが、これは質問機能に直接は関係ない変更だったりします。
日記や問題・選択肢を生成してBigQueryに追加する処理が結構かかるので、応答待ちの間にローディングアニメーションつけようと思ってついでに追加しました。

### 環境変数等

```diff_python:main.py
'''
環境変数
'''
# CHANNEL_ACCESS_TOKEN
channel_access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
# CANNEL_SECRET
channel_secret = os.environ.get('LINE_CHANNEL_SECRET')
# GEMINI_APIKEY
gemini_api_key = os.environ.get('GEMINI_API_KEY')
# 日記作成のシステムプロンプト
system_prompt_diary = f"""
与えられた文章から日記を生成し、JSON形式で答えてください。
その際、以下の条件に沿って回答してください。
1. 与えられた文章に場面描写や心情、セリフを追加して、最大300文字以内の短い物語にしてください。登場人物のキャラクター設定は以下に従ってください。
    ・主体となる人物は多少皮肉っぽい、演劇的な言葉選びや言い回しにしてください
    ・その他の人物は以下の例の中からランダムに性格付けしてください
    例：・温和でお人よし、主体となる人物の皮肉っぽい言い回しをたしなめる
        ・陽気でそそっかしい、主体となる人物を振り回す
        ・謎めいて陰を感じさせ、含みのある物言いをする
        ・高圧的でプライドが高く、主体となる人物を下に見ている
        ・内向的で卑屈なところがあり口数が少ない
        ・華やかで気品があり、人の目を引く振る舞いをする
2. 次に、物語を英文にしてください。英文作成の際、探偵小説のような文体にしてください。
3. 上記2.の英文をさらに和訳してください。
4. 英文をもとに、3問の3択問題を作成してください。
    ・問題文と選択肢を英語で作成してください。
    ・正答番号を指定してください。選択肢の1番目が正解なら1、2番目が正解なら2...というようにしてください。
    ・正解解説を日本語で作成してください
5. JSON形式で以下の通り項目を設定してください。
    original : 2.で作成した英文。
    translation : 3.で作成した訳文。
    exercises : 4.で作成した問題のリスト（3問）。内容項目はquestion_no 、question、options 、answer 、explanation 。
        question_no : 問題番号。1からカウントする。
        question : 4.で作成した問題文。
        options : 4.で作成した選択肢のリスト（3択）。内容項目はoption_no、option 。
            option_no : 選択肢番号。1からカウントする。
            option : 選択肢のテキスト。
        answer : 4.で作成した問題の正解。option_noに合わせる。
        explanation : 4.で作成した問題の日本語解説文。
"""
+ # 質問用のシステムプロンプト
+ system_prompt_asking = """
+ 以下の英文について質問されるので、和訳の内容も踏まえて100字程度で回答してください。
+ 英文：{english_text} 
+ 和訳：{japanese_text}
"""
```

質問用のシステムプロンプトを追加しました。
単に単語等をそのまま投げると文脈とは異なる回答になりそうだったので、これを踏まえて答えてね、という指示を出すためです。

### メイン処理
まずメッセージ受信直後にローディングアニメーションの処理を追加します。

```diff_python:main.py
    for event in events:
        # 返信用変数準備
        reply_data = []
+       # ローディングアニメーション
+       url_loading = 'https://api.line.me/v2/bot/chat/loading/start'
+       headers_loading = {
+           'Content-Type': 'application/json',
+           "Authorization": f'Bearer {channel_access_token}'
+       }
+       payload_loading = {
+           "chatId": event.source.user_id,
+           "loadingSeconds": 40
+       }
        if isinstance(event, MessageEvent):
            # メッセージを受信した場合
+           response = requests.post(url_loading, headers=headers_loading, data=json.dumps(payload_loading)) 
            if isinstance(event.message, TextMessage):
```

Pythonでのローディングアニメーション処理については[こちら](https://qiita.com/mamoru-ngy/items/5a6a4a9d90db5567a3cf)の記事を参考にしました。

https://qiita.com/mamoru-ngy/items/5a6a4a9d90db5567a3cf

待ち時間は40秒と長めにしています。

ここからが質問機能に関する変更

```diff_python:main.py
                # 日記データを検索
                diary = querys.select_diary(user_status.current_diary_id)
                if user_status.status == '1' :
                #（長いので処理を省略）
+               elif user_status.status == '2':
+                   # 質問中の場合、AI応答を生成して応答を返す
+                   response = generate_ai_message(event.message.text, "text/plain", 
+                           system_prompt_asking.format(english_text=diary.english_text, 
+                           japanese_text=diary.japanese_text))
+                   response += '\n' + '（ほかに質問があれば続けてください）'
+                   quick_action = [QuickReplyButton(action=PostbackAction(label='問題を解く', data='try_to_answer',display_text='問題を解く'))]
+                   reply_data.append(TextSendMessage(text=response,quick_reply=QuickReply(items=quick_action)))
                else:
                    # 上記以外の場合、クイックリプライにPostbackアクションを入れたボタンを作る
-                   read = [QuickReplyButton(action=PostbackAction(label='既読', data='read',display_text='読みました'))]
+                   quick_action = [QuickReplyButton(action=PostbackAction(label='問題を解く', data='try_to_answer',display_text='問題を解く'))
+                       ,QuickReplyButton(action=PostbackAction(label='質問する', data='ask_question',display_text='質問する'))]
                    # 日記の英文をメッセージに編集
-                   reply_data.append(TextSendMessage(text=diary.english_text,quick_reply=QuickReply(items=read)))
+                   reply_data.append(TextSendMessage(text=diary.english_text,quick_reply=QuickReply(items=quick_action)))
```
まず、出題前の英文を返すところでつけるクイックリプライを「既読」から「問題を解く」「質問する」の２つに変更しました。
これで次のアクションを出題にするか質問への回答とするかを分けます。
そして質問に回答するステータスとして「2」を設定し、その場合は質問への回答と「問題を解く」のクイックリプライを付けるようにしました。

そして最後にポストバックアクションの切り分けです。

```diff_python:main.py
        elif isinstance(event, PostbackEvent):
-           # ポストバックイベントの場合、1問目を出題
+           # ポストバックイベントの場合、ユーザーステータスを取得
            user_status = querys.select_user_status(event.source.user_id)
            # 日記データを取得
            diary = querys.select_diary(user_status.current_diary_id)
+           if event.postback.data == 'try_to_answer':
+               # 問題を解く場合、1問目を出題
                # 処理中の問題番号を更新
                user_status.current_question_no = 1
                # 問題メッセージを編集
                reply_data.append(edit_question(diary.id, user_status.current_question_no))
                # ユーザーステータスを出題中にする
                user_status.status = '1'
+           elif event.postback.data == 'ask_question':
+               # 質問する場合、ユーザーステータスを質問中にする
+               user_status.status = '2'
+               # 返信メッセージを編集
+               reply_data.append(TextSendMessage(text='質問をどうぞ。'))

            # ユーザーステータス更新
            querys.update_user_status(user_status)
            # 応答内容をLINEで送信
            line_bot_api.reply_message(event.reply_token, reply_data) 

    return jsonify({ 'message': 'ok'})
```

dataに設定した値が何かによって、出題にするか質問回答するかを判定しました。
動かすとこんな感じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/871ee3f0-1774-3aaa-d6fc-8b12622c5d66.jpeg" width="60%">