方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【ChatGPT×LINE】リッチメニューで複数方言対応:方言切り替え記憶を保存

この記事は約17分で読めます。

現状だと、方言切り替えを保存できていない問題

現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

ミーア
おしゃべり猫型ロボット「ミーア」は、100以上の種類の豊かな表情で、悲しみや喜びを共有します。様々な性格(皮肉・おせっかい・ロマンチスト・天然)や方言(大阪弁・博多弁・鹿児島弁・広島弁)も話してくれます。ミーアとの暮らしで、毎日の生活をもっ...

前回、下記記事で、LINEリッチメニューで複数方言に対応できるようにしたのだが、方言切り替えが保存されずに、標準語での応答に戻ってしまう問題を解決していなかった。

現在のコードでは、ユーザーが方言を選択するとhandle_postback関数がトリガーされ、選択された方言に基づいて応答が生成される。しかし、その後のテキストメッセージが送信されるとhandle_message関数がトリガーされ、ここでは方言の設定が考慮されていないため、応答が標準語に戻ってしまう。

Python
import os
from datetime import datetime

from flask import Request, abort
from google.cloud import firestore
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, PostbackEvent
from openai import OpenAI

line_bot_api = LineBotApi(os.environ["LINE_CHANNEL_ACCESS_TOKEN"])
handler = WebhookHandler(os.environ["LINE_CHANNEL_SECRET"])
db = firestore.Client()
client = OpenAI()

USE_HISTORY = True


def get_previous_messages(user_id: str) -> list:
    query = (
        db.collection("users")
        .document(user_id)
        .collection("messages")
        .order_by("timestamp", direction=firestore.Query.DESCENDING)
        .limit(3)
    )
    return [
        {
            "chatgpt_response": doc.to_dict()["chatgpt_response"],
            "user_message": doc.to_dict()["user_message"],
        }
        for doc in query.stream()
    ]


def format_history(previous_messages: list) -> str:
    return "".join(
        f"ユーザー: {d['user_message']}nアシスタント: {d['chatgpt_response']}n"
        for d in previous_messages[::-1]
    )


def load_prompt(dialect: str) -> str:
    # 方言に対応するファイルパスを構築
    filename = f"dialect_prompt/{dialect}_prompt.txt"

    # ファイルからプロンプト文を読み込む
    with open(filename, "r", encoding="utf-8") as file:
        prompt = file.read()
    return prompt


def generate_response(user_message: str, history: str, dialect: str = None) -> str:
    if dialect:
        # 方言に基づいたプロンプトを読み込む
        prompt = load_prompt(dialect)
    else:
        # 通常のプロンプトを使用
        prompt = "ここに通常のプロンプトを設定"

    if USE_HISTORY and history:
        history_section = f"これまでの会話履歴:n{history}n"
    else:
        history_section = ""
    full_prompt = f"{prompt}n{history_section}nユーザーからのメッセージ: {user_message}"

    # OpenAI APIを使用して応答を生成
    chatgpt_response = client.chat.completions.create(
        model="gpt-3.5-turbo-1106",
        messages=[{"role": "system", "content": full_prompt},
                  {"role": "user", "content": user_message}],
        temperature=0.2,
    )
    print(chatgpt_response)

    print(type(chatgpt_response))

    # 応答のテキスト部分を取得
    return chatgpt_response.choices[0].message.content


def reply_to_user(reply_token: str, chatgpt_response: str) -> None:
    line_bot_api.reply_message(
        reply_token, TextSendMessage(text=chatgpt_response))


def save_message_to_db(user_id: str, user_message: str, chatgpt_response: str) -> None:
    doc_ref = db.collection("users").document(
        user_id).collection("messages").document()
    doc_ref.set(
        {
            "user_message": user_message,
            "chatgpt_response": chatgpt_response,
            "timestamp": datetime.now(),
        }
    )


def main(request: Request) -> str:
    signature = request.headers["X-Line-Signature"]
    body = request.get_data(as_text=True)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return "OK"


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event: MessageEvent) -> None:
    user_id = event.source.user_id
    user_message = event.message.text

    if USE_HISTORY:
        previous_messages = get_previous_messages(user_id)
        history = format_history(previous_messages)
    else:
        history = None
    chatgpt_response = generate_response(user_message, history)

    reply_to_user(event.reply_token, chatgpt_response)
    if USE_HISTORY:
        save_message_to_db(user_id, user_message, chatgpt_response)


@handler.add(PostbackEvent)
def handle_postback(event: PostbackEvent):
    user_id = event.source.user_id
    data = event.postback.data
    dialect = data.split('=')[1] if '=' in data else None

    if USE_HISTORY:
        previous_messages = get_previous_messages(user_id)
        history = format_history(previous_messages)
    else:
        history = None

    chatgpt_response = generate_response("", history, dialect)
    reply_to_user(event.reply_token, chatgpt_response)
    if USE_HISTORY:
        save_message_to_db(user_id, "POSTBACK: " + data, chatgpt_response)

というわけで、ユーザーがLINEリッチメニューで選択した方言情報を保持しておかないといけない。

方言選択をFirestoreに保存

  1. ユーザーが方言を選択すると、その選択をFirestoreデータベースに保存する。これにより、ユーザーの方言設定をセッション間で保持できる。
  2. ユーザーからの各テキストメッセージを処理する際に、データベースからユーザーの現在の方言設定を取得し、それに基づいて応答を生成する。

データベースにユーザーの方言設定を保存する

Python
def save_dialect_to_db(user_id: str, dialect: str) -> None:
    user_ref = db.collection("users").document(user_id)
    user_ref.set({"dialect": dialect}, merge=True)

指定されたユーザーIDドキュメント内にdialectフィールドを設定または更新する

ユーザーの方言設定を取得する

Python
def get_user_dialect(user_id: str) -> str:
    user_ref = db.collection("users").document(user_id)
    doc = user_ref.get()
    if doc.exists:
        return doc.to_dict().get("dialect")
    return None

handle_postback で方言設定を保存

Python
@handler.add(PostbackEvent)
def handle_postback(event: PostbackEvent):
    user_id = event.source.user_id
    data = event.postback.data
    dialect = data.split('=')[1] if '=' in data else None

    save_dialect_to_db(user_id, dialect)  # ここで方言を保存

    # 以下は以前と同様

handle_message で方言設定を利用

Python
@handler.add(PostbackEvent)
def handle_postback(event: PostbackEvent):
    user_id = event.source.user_id
    data = event.postback.data
    dialect = data.split('=')[1] if '=' in data else None

    if USE_HISTORY:
        previous_messages = get_previous_messages(user_id)
        history = format_history(previous_messages)
    else:
        history = None

    save_dialect_to_db(user_id, dialect)  # 方言を保存
    # 保存した方言を再取得
    updated_dialect = get_user_dialect(user_id)

    chatgpt_response = generate_response("", history, updated_dialect)
    reply_to_user(event.reply_token, chatgpt_response)
    if USE_HISTORY:
        save_message_to_db(user_id, "POSTBACK: " + data, chatgpt_response)

これらの変更により、ユーザーが方言を選択した際にその選択が維持され、後続のメッセージでその方言が利用されるようになる。

方言を保存した後に、すぐに保存した方言を再取得しないと、最初の方言切り替え(LINEリッチメニューのアイコンを押した時の返信)が、前の方言のままになってしまう。

Cloud Functionsにデプロイして確認

左がBeforeで、右がAfter。

違いが分かりにくいと思うが、左は、鹿児島弁のLINEリッチメニューアイコンをクリックした直後は、鹿児島弁で返答が来ているが、その後「最近何してた?」と聞くと、標準語で返している。

一方で、右は、鹿児島弁で返し続けている。

Firestoreのログでも、今回新しくuser_id配下にmessagesコレクションと同階層でdialectが追加されている。

これで、無事、LINEリッチメニュアイコンでの方言切り替えに対応できるようになった。

リッチメニューのアイコンをクリックした時に、テキスト表示

次に、リッチメニューのアイコンをクリックした時に、「鹿児島弁で会話しましょう」というテキストを表示するように変更する。

Messaging APIのポストバックアクションの、displayTextに、ユーザーのメッセージとしてLINEのトーク画面に表示されるテキストを追加する。

Messaging APIリファレンス
LINE Developersサイトは開発者向けのポータルサイトです。LINEプラットフォームのさまざまな開発者向けプロダクトを利用するための、管理ツールやドキュメントを利用できます。LINEログインやMessaging APIを活用して、...

ついでに、inputOptionで、キーボードをデフォルトで開くように追加する。

各リッチメニューアイコンのaction時のjsonを下記のように修正。

Python
"action": {
      "type": "postback",
      "data": "dialect=kyoto",
      "displayText": "京都弁で話ししましょう。",
      "inputOption": "openKeyboard",
  }

あとは、下記に沿って、リッチメニューの画像をアップロード

実際に、鹿児島弁のミーアちゃんをタップすると、左図のように「かごんま弁で話ししましょう」とテキストが表示され、キーボードが自動で立ち上がりテキスト入力モードになった。

ちなみに京都弁に関しては、まだプロンプトファイルを作成していないので、レスポンスはないのが正常状態。

方言を切り替えた時に会話履歴をクリア

これは仕様の決めの問題にはなるが、ユーザーが方言を変更した際に、その変更を反映したユーザーの入力(例:「博多弁で話しましょう」)を取得し、新しい方言に基づいた応答を生成するようにする。また、会話履歴をリセットすることで、前の方言の影響を受けることなく、新しい方言での対話を始めることができるようにする。

ユーザーの会話履歴をクリアする関数を追加

ユーザーの会話履歴をクリアする関数を実装するには、指定されたユーザーのFirestoreのサブコレクションにある会話履歴を削除する必要がある。

この関数では、指定されたユーザーIDに対応する"messages"サブコレクション内のドキュメントを取得し、それぞれ削除している。この処理により、ユーザーの会話履歴が完全にクリアされる。

Python
def reset_user_history(user_id: str) -> None:
    # ユーザーの会話履歴を含むサブコレクションの参照を取得
    messages_ref = db.collection("users").document(user_id).collection("messages")

    # サブコレクション内のドキュメントを取得
    docs = messages_ref.stream()

    # 各ドキュメントを削除
    for doc in docs:
        doc.reference.delete()

    # 必要に応じて、ユーザーのステータスや設定などもリセットする処理をここに追加

handle_postback関数を下記に修正。方言が変更されたときだけ会話履歴をリセットするようにする。

Python
@handler.add(PostbackEvent)
def handle_postback(event: PostbackEvent):
    user_id = event.source.user_id
    data = event.postback.data
    dialect = data.split('=')[1] if '=' in data else None

    # 保存済みの方言を取得
    current_dialect = get_user_dialect(user_id)

    # 新しい方言で設定された場合、会話履歴をリセット
    if dialect != current_dialect:
        reset_user_history(user_id)
        history = None
    elif USE_HISTORY:
        previous_messages = get_previous_messages(user_id)
        history = format_history(previous_messages)
    else:
        history = None

    save_dialect_to_db(user_id, dialect)  # 方言を保存

    # 保存した方言を再取得
    updated_dialect = get_user_dialect(user_id)

    chatgpt_response = generate_response("", history, updated_dialect)
    reply_to_user(event.reply_token, chatgpt_response)

    # 方言が変わっていない場合のみ、メッセージを保存
    if USE_HISTORY and dialect == current_dialect:
        save_message_to_db(user_id, "POSTBACK: " + data, chatgpt_response)

検証

これで、方言を切り替えずに会話を続けている場合は、会話記憶をfirestoreに保存し続け、文脈を理解(これは今までと一緒)

方言を切り替えると、firestoreでは会話履歴が消去され、dialectがkagoshhimaからhakataに切り替わる。

そして、博多弁で会話し続けることができるようになった。

コメント

タイトルとURLをコピーしました