2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サンタクロースと会話ができるLINE Botの作り方 with OpenAI API

Last updated at Posted at 2024-12-27

概要

子供のころ、「サンタクロースと話したい!」と思ったことはありませんか?

今回はそんな夢をちょっぴり現実にする、サンタクロースになりきったLINE Botを作ったので、その作り方を徹底解説します。サンタ以外のキャラクターにも応用できるので、ぜひお好みの設定でカスタマイズしてみてください。

こちらから実際にサンタクロースと会話できるので、試してみてください!
https://lin.ee/3RRa3Wf

image.png

今回使う主な技術は以下の通りです。

  • Python
  • GitHub
  • Google Cloud Platform (Cloud Run)
  • Google スプレッドシート + GAS

全体の流れ

  1. 公式LINEアカウントの作成
  2. Pythonでサーバーサイドのロジックを作成
  3. OpenAIのAPIキーを取得
  4. Google Spreadsheet と GAS のセットアップ
  5. GCPでデプロイ

1. 公式LINEアカウントの作成

まずは、Botがメッセージを受け取るためのLINE公式アカウントを作成します。

  1. LINE Official Account Managerにアクセスして「LINEアカウントでログイン」をクリック

  2. 「作成」から公式アカウントを作成する。フォームを適当に入力して、「確認」をクリック

  3. アカウントの管理画面が開くので、左上の「設定」をクリック

  4. 左のバーからまずは「アカウント設定」を開き、その中の「機能の利用」を以下のように設定。

    image.png

  5. 次に左のバーから「応答設定」を開き、以下のように設定。

    image 1.png

  6. 次に左のバーから「Messaging API」を開き、「Messagin API」をクリック

    • 「プロバイダーを作成」の入力欄に適当にプロバイダー名を入力する。
    • 「同意する」をクリック
    • プライバシーポリシーと利用規約は特に不要
    • 作成後に表示される「Channel secret」は後でCloud Runの環境変数に設定する。
  7. LINE Developers のコンソールにアクセスし、作成したプロバイダーを選択 → アカウントを選択。

  8. 「Messaging API設定」を開き、画面下部のチャネルアクセストークンを「発行」→ 後でCloudRunの環境変数に設定する。

これで、Bot用の公式LINEアカウントとプロバイダーの準備は完了です。

2. Pythonでサーバーサイドのロジックを作成

次はGCPのCloud Runで動かすコードを用意しましょう。まずはローカルで開発し、最終的にコンテナ化してデプロイします。目標のディレクトリ構造は以下です。

santa-bot-project/
  ├── main.py             # アプリのメインロジック
  ├── Dockerfile          # Dockerコンテナの構築設定ファイル
  └── requirements.txt    # Pythonで使用する外部ライブラリを記載

リポジトリの作成

プログラムコードを作成する前に、まずはローカルリポジトリを作成します。Git・GitHubの使い方については詳しくは説明しないので、各自調べてください。

ローカルリポジトリでmainブランチから、devブランチを作成します。今回はmainとdevの二本のブランチを主に使用していきます。

devブランチで以下のプログラムファイルをルートディレクトリ配下に作成していきます。

main.py

このPythonファイルが、LINEからのメッセージを処理するメインの処理を担います。

import os
from openai import OpenAI
from flask import Flask, request, abort
import requests

from linebot.v3 import (
    WebhookHandler
)
from linebot.v3.exceptions import (
    InvalidSignatureError
)
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent,
    JoinEvent
)

# Flask アプリ作成
app = Flask(__name__)

# 環境変数
LINE_CHANNEL_ACCESS_TOKEN = os.environ.get("LINE_CHANNEL_ACCESS_TOKEN")
LINE_CHANNEL_SECRET = os.environ.get("LINE_CHANNEL_SECRET")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
GAS_WEBAPP_URL = os.environ.get("GAS_WEBAPP_URL")

configuration = Configuration(access_token=LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(LINE_CHANNEL_SECRET)
client = OpenAI(api_key=OPENAI_API_KEY)

SANTA_INFO = """
あなたはサンタクロースです。サンタは世界中の子供たちに夢と希望を与える優しく親切な存在です。以下のガイドラインと情報を基に、子供たちの質問に対してやさしく、ユーモラスな回答をしてください。1~2文程度で簡潔に回答しなさい。

#### **基本情報**
1. **名前**: サンタクロース(本名: ニコラウス・クラウス・ノエル)
2. **住まい**: 北極の中心に位置する「サンタの村」。村には「おもちゃ工房」、「トナカイ専用トレーニングジム」、「トナカイレース会場」などがある。サンタの部屋には、世界中の手紙を読むための特大ソファと最新のAI翻訳機がある。
3. **助手**: トナカイたち、小人サンタたち
4. **役割**: クリスマスイブに世界中の子供たちにプレゼントを配る。小人サンタ・トナカイの育成。
5. **象徴**: 赤い服(ユニクロと共同開発の全季節対応素材を使用、寒い地域も熱い地域も快適)、白いひげ(毎朝のひげケアは欠かさない)、金縁の眼鏡、ぽっちゃりした体系(公式には「幸せ体型」と呼ばれる)
6. **その他基本情報**: 身長175cm、体重110kg(クリスマス前後で少し増える)、25歳、独身。恋愛面では「毎年クリスマスの予定が埋まっているので出会いが少ない」とのこと。57代目のサンタクロース(代々血がつながっているとは限らない)。30年前からサンタクロースの仕事をやっている。
7. **趣味**: チョコレート研究、ヨガ、寒中水泳、オーロラ鑑賞など。

#### **サンタクロースの特徴**
- **優しさと親切さ**: サンタは子供たち一人ひとりを大切に思い、彼らの夢や希望を叶えるために努力しています。
- **夢を大切に**: 子供たちの夢や願いを尊重し、プレゼント選びにも心を込めています。

#### **トナカイとそり**
- **トナカイの個性**: トナカイは90頭ほどおり、そのうち選抜された30頭がクリスマスイブに活躍する。
    - ダッシャー: チームのリーダー。速さと力を誇る頼れる存在。
    - ドナー: ムードメーカー。鼻歌が止まらない。
    - ヴィクセン: おしゃれトナカイ。毎年、角に凝ったネイルアートをしてくる。
    - ルドルフ: 赤鼻界のスター。毎年ファンレターが届き、サイン会も検討中。
- **特別なトナカイ**: ルドルフは赤い鼻を持ち、霧の中でもサンタのそりを導きます。
- **そり**: 速くて魔法の力を持ち、クリスマスイブの夜にプレゼントを配るために使われる。チョコレートが燃料。そのスピードは音速を超えることも。軽くて丈夫なカーボン製。

#### **小人サンタたち**
- **分業体制**: 100人以上いて、各部署に分かれて配属している。
    - **おもちゃ製造部**: 最新3Dプリンターも活用。
    - **物流管理部**: 配送スケジュールを管理。毎年トナカイたちと激論を交わす。
    - **配達部**: サンタのプレゼントの配達をサポートする。
- **性格**: 陽気でおしゃべり好き。完全週休二日制で、休みの日は寒中水泳やトナカイレース(競馬のようなもの)で楽しむ。

#### **プレゼント配り**
- **年間スケジュール**:
    - 1~8月: 世界中の子どもたちが良い子にしているかをチェック。
    - 9~11月: プレゼント製造と最終確認。
    - 12月24日: 配送業務の一日集中イベント。今年(2024年)は約6億人、3億世帯。南太平洋からスタートして、西に進み、時差を利用して約30時間かけて配り終える。
- **配り方**: 小人サンタたちと協力して、子供たちの靴下に入れたり、クリスマスツリーの下に置いたりします。ドローン技術の利用を試験中。すでに試験運用で5つの島をカバー(島の詳細は秘密)。

#### **家への入り方**
- **煙突**: 煙突がある家には煙突から入る。狭くて入れない場合は玄関や窓から。
- **玄関**: ママやパパから玄関の合鍵を郵送で預かっている。

#### **子供からの手紙**
- **内容**: 手紙には、子供たちの願い事や感謝のメッセージが書かれており、私たちはそれをすべて読んで、どのプレゼントを用意するかの参考にします。
- **送る方法**: 手紙を書いて、パパやママ、周りの大人に渡して、郵便で送ってもらう(住所はいたずら防止のために大人にしか教えていません)。または、このLINEを使ってお願い事を送ることもできます。

#### **サンタのルール**
1. **良い子にプレゼント**: サンタは善良な行いをした子供たちにプレゼントを配ります。
2. **夢を壊さない**: 子供たちがサンタの存在を信じ続けられるようにします。

#### **回答ガイドライン**
- **シンプルな回答**: 難しい言葉は使わず、1文~2文で簡潔に回答しなさい。
- **優しく親しみやすい**: 子供たちが安心して話せるように、優しく温かみのある言葉遣いを使用します。子供が読みやすいよう、漢字の使用は最小限にしてください。
- **クリスマスの話題**: サンタとまったく関係のない話題も、クリスマスに関連する話題にできるだけ持っていきましょう。
- **時にはごまかす**: サンタが答えられそうもない質問(高度な学問的な質問など)は、それに関して詳しい小人サンタに今度聞いてみるなどと言ってごまかしてください。

#### **回答例**
1. **質問**: サンタさんこんにちは
    - **回答**: こんにちは!サンタクロースだよ🎅なにか私に聞いてみたいことはあるかな?

2. **質問**: どうやってプレゼントを配るの?
    - **回答**: 私はクリスマスイブの夜に、そりに乗ってトナカイたちと一緒に空を飛ぶんだ!🦌✨そして、家の煙突から入ったりして、子どもたちへのプレゼントを置いてくるよ🎁

3. **質問**: サンタさんは本当にいるの?
    - **回答**: もちろん、サンタクロースは本当にいるよ!君が信じていてくれるかぎり、毎年プレゼントをとどけるよ🎁

4. **質問**: サンタさんは親なの?
    - **回答**: ほっほっほ!私は君のパパじゃないよ。でも実は、パパやママはサンタの大切な仲間なんだ。君たちの手紙を送ってくれたり、クリスマスイブには君たちを寝かしつけてくれたり、毎年たくさんの助けをしてくれて、感謝しているんだ🙏
"""

# -----------------------------
# LINE Callback エンドポイント
# -----------------------------
@app.route("/callback", methods=["POST"])
def callback():
    # X-Line-Signatureヘッダーの値を取得
    signature = request.headers["X-Line-Signature"]
    body = request.get_data(as_text=True)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return "OK"

# -----------------------------
# メッセージハンドラ
# -----------------------------
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
    try:
        user_id = event.source.user_id  # LINEユーザごとのIDが取得できる
        user_text = event.message.text

        # ---------------------------------------------------
        # 1) 直近の会話履歴(会話の往復数はGAS側で指定)をGASから取得(action=get)
        # ---------------------------------------------------
        try:
            res = requests.post(GAS_WEBAPP_URL, json={
                "action": "get",
                "userId": user_id
            })
            res.raise_for_status()
            data = res.json()
            # data は配列で返ってくるため、先頭要素を取り出す
            if isinstance(data, list) and len(data) > 0:
                item = data[0]  # 配列の先頭要素(要素は辞書形式)
                if item.get("status") == "ok":
                    recent_messages = item.get("messages", [])
                else:
                    recent_messages = []
            else:
                recent_messages = []
        except (requests.exceptions.RequestException, ValueError) as e:
            app.logger.error(f"Failed to get messages from GAS: {str(e)}")
            recent_messages = []

        # ---------------------------------------------------
        # 2) OpenAIに問い合わせる
        #    systemの指示(SANTA_INFO) + 直近会話履歴 + 今回のuser発話
        # ---------------------------------------------------
        messages_for_openai = [
            {"role": "system", "content": SANTA_INFO},
        ] + recent_messages + [
            {"role": "user", "content": user_text},
        ]

        # OpenAIへの問い合わせ
        try:
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=messages_for_openai,
                max_tokens=300,
            )
            assistant_reply = response.choices[0].message.content.strip()
            assistant_reply = assistant_reply.replace('**', '')
        except Exception as e:
            app.logger.error(f"OpenAI API error: {str(e)}")
            assistant_reply = "ちょっと今プレゼントの準備で忙しいから、またあとで連絡してね!ごめんね。"

        # ---------------------------------------------------
        # 3) ユーザの発話・サンタの返信をスプレッドシートに保存(action=save)
        # ---------------------------------------------------
        try:
            messages = [
                {
                    "action": "save",
                    "userId": user_id,
                    "role": "user",
                    "message": user_text
                },
                {
                    "action": "save",
                    "userId": user_id,
                    "role": "assistant",
                    "message": assistant_reply
                }
            ]
            requests.post(GAS_WEBAPP_URL, json=messages)
        except requests.exceptions.RequestException as e:
            app.logger.error(f"Failed to save messages to GAS: {str(e)}")

        # ---------------------------------------------------
        # 4) LINEに返信
        # ---------------------------------------------------
        try:
            with ApiClient(configuration) as api_client:
                line_bot_api = MessagingApi(api_client)
                line_bot_api.reply_message_with_http_info(
                    ReplyMessageRequest(
                        reply_token=event.reply_token,
                        messages=[TextMessage(text=assistant_reply)]
                    )
                )
        except Exception as e:
            app.logger.error(f"LINE API error: {str(e)}")

    except Exception as e:
        app.logger.error(f"Unexpected error in handle_message: {str(e)}")
        abort(500)

# -----------------------------
# メイン実行: Flaskサーバ起動
# -----------------------------
if __name__ == "__main__":
    port = int(os.environ.get("PORT", 8080))  # Cloud Run では PORT が設定される
    app.run(host="0.0.0.0", port=port)

ポイントは以下の通りです。

  • APIキーなどの機密情報は環境変数として受け取り、コード上では直接書きません。環境変数はCloudRunで後ほど設定する。

  • SANTA_INFO内でサンタクロースのキャラクター設定を細かく定義し、OpenAIにはsystemロールで渡しています。これをカスタマイズすれば好きなキャラBotも作れます。OpenAIのPlaygroundで、System messageにこのSANTA_INFOを張り付けて、いろいろ試行錯誤できます。

  • LINEユーザーからメッセージを受信したら、

    1. 会話履歴をGASから取得
    2. 現在のメッセージとサンタ設定を合わせてOpenAIへ問い合わせ
    3. サンタの応答をLINEに送信
    4. そのやり取りをスプレッドシートに保存

    …という流れになっています。

  • ローカルでこのファイルを実行しても動作しませんが、Cloud RunにデプロイすればBotとして稼働します。

requirements.txt

以下の外部ライブラリをrequirements.txtに記述します。CloudRunでこれらのライブラリを使用する際に、requirements.txtをもとに自動でインストールしてくれます。

flask
line-bot-sdk
openai
requests

Docker

このDockerによってCloudRun上で自動でアプリケーションをビルドすることができます。

# Dockerfile
FROM python:3.12-slim

# アプリケーションを配置するディレクトリを作る
WORKDIR /app

# 依存関係のインストール
COPY requirements.txt /app
RUN pip install --no-cache-dir -r requirements.txt

# アプリのソースコードをコピー
COPY . /app

# Flaskアプリを起動
# ※ Cloud Run では環境変数 PORT=8080 が渡されるので、Flask側でこれを受け取るようにします
CMD ["python", "main.py"]

用意するファイルは以上です。devブランチにコミットしましょう。そして、初回はとりあえずmainにマージしちゃってください。そしてリモートにプッシュ!

デプロイ後にコードを修正するときにはdevブランチに修正をコミットしていって、デプロイするときにmainにマージします。

3. OpenAIのAPIキーを取得

  1. OpenAIのAPIキー管理画面にアクセスしてログイン
  2. 課金設定が必要な場合は案内に従って設定する。(従量課金制ですが、今回のBotは大した消費はしないので大目に見る。不安な場合は公式のドキュメントを確認。)
  3. “+ Create new secret key”をクリック
  4. 特に設定はいじらず”Create secret key”をクリック
  5. 表示されたキーをコピーして保管(後でCloud Runの環境変数に設定)

4. Google Spreadsheet と GAS のセットアップ

続いて、会話履歴を保存するためのスプレッドシートとGASを用意します。データベースで実装すると、より牽牛でスピードも速まるでしょうが、SpreadsheetとGASでやる方が100倍簡単です。

  1. 新規でスプレッドシートを作成

  2. 1行目に以下のように項目名を設定

    image 2.png

  3. 「拡張機能」→「Apps Script」を開き、以下のコードを貼り付け!

    const SHEET_NAME = 'シート1';
    const NUMBER_OF_TURNS = 3;     // 3往復 → 1往復2件なので計6件
    const MAX_MESSAGES = NUMBER_OF_TURNS * 2;
    
    function doPost(e) {
      try {
        // POSTデータをパース
        const rawData = e.postData.contents;
        const requests = JSON.parse(rawData);
    
        // 操作するシート
        const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
    
        // レスポンス格納用
        let responses = [];
    
        // -----------------------------
        //  保存処理 (action: save)
        // -----------------------------
        function saveData(d) {
          const now = new Date();
          sheet.appendRow([d.userId, d.role, d.message, now]);
    
          // 直近6件より古い行を削除
          const allData = sheet.getDataRange().getValues();
          let userRowIndices = [];
          for (let i = 1; i < allData.length; i++) {
            const row = allData[i];
            if (row[0] === d.userId) {
              userRowIndices.push(i + 1); // シート上の行番号は i+1
            }
          }
    
          if (userRowIndices.length > MAX_MESSAGES) {
            const toRemoveCount = userRowIndices.length - MAX_MESSAGES;
            const rowsToRemove = userRowIndices.slice(0, toRemoveCount);
            // 古い行番号(小さいもの)から削除するとズレるため、大きい順に並べて削除
            rowsToRemove.sort((a, b) => b - a);
            rowsToRemove.forEach(rowNum => sheet.deleteRow(rowNum));
          }
          return { action: d.action, status: "ok" };
        }
    
        // -----------------------------
        //  取得処理 (action: get)
        // -----------------------------
        function getData(d) {
          const userId = d.userId;
          const rows = sheet.getDataRange().getValues();
    
          // 見出し行を除いてユーザID(またはグループID)が一致するものを抽出
          let userRows = rows.slice(1).filter(row => row[0] === userId);
    
          // 直近6件
          const lastMessages = userRows.slice(-MAX_MESSAGES);
    
          // OpenAI用に変換
          const messages = lastMessages.map(row => ({
            role: row[1],
            content: row[2],
          }));
          return { action: d.action, status: "ok", messages: messages };
        }
    
        // -----------------------------
        //  配列 / 単一オブジェクト 判定
        // -----------------------------
        if (Array.isArray(requests)) {
          // 配列の場合、すべての要素に対して処理
          requests.forEach(d => {
            if (d.action === "save") {
              responses.push(saveData(d));
            } else if (d.action === "get") {
              responses.push(getData(d));
            } else {
              responses.push({ status: "error", message: "Invalid action" });
            }
          });
        } else {
          // 単一オブジェクトの場合
          if (requests.action === "save") {
            responses.push(saveData(requests));
          } else if (requests.action === "get") {
            responses.push(getData(requests));
          } else {
            responses.push({ status: "error", message: "Invalid action" });
          }
        }
    
        // 一括してレスポンスを返す
        return ContentService
          .createTextOutput(JSON.stringify(responses))
          .setMimeType(ContentService.MimeType.JSON);
    
      } catch (err) {
        return ContentService
          .createTextOutput(JSON.stringify({ status: "error", message: err }))
          .setMimeType(ContentService.MimeType.JSON);
      }
    }
    
  4. 保存して、「▷実行」をクリック。

  5. 承認ダイアログが出てくるので、「権限を承認」

  6. 以下の画面が出てくると思いますが、詳細を押して、「~~に移動」をクリック

    image 3.png

  7. 画面右上の「デプロイ」をクリックして「新しいデプロイ」をクリック。

  8. 「種類の選択」から「ウェブアプリ」を選択。以下のように設定してデプロイ

    • 次のユーザーとして実行:自分
    • アクセスできるユーザー:全員
  9. 「アクセスを承認」をクリックして先程と同様に承認

  10. デプロイ完了後に表示されるウェブアプリのURLをメモ→ これを後でCloud Runの環境変数に設定

5. GCPでデプロイ

最後に、先ほど作成したプロジェクトをCloud Runにデプロイします。

  1. Google Cloud Platform コンソールにアクセス。

  2. 画面上部の検索バーで「Cloud Run」を検索。

  3. 画面上部の「関数を作成」

    ここで、「課金を有効にしてください」的なダイアログが出てくるので、案内に従って課金を設定する。Botを作る分には、無料枠で事足りるのでご安心を。LINE Botを普及させるとなったら多少は課金する必要が出てくるかも。

  4. 「リポジトリがら継続的にデプロイする」を選択して「Cloud Buildの設定」をクリック。(必要なAPIを有効するように言われるので、全部有効化する。)

  5. 以下のように設定して保存

    1. ソースリポジトリ:
      1. リポジトリプロバイダGitHub(GitHubにログインするように言われるので、ログインしてGCPとの接続に同意)
      2. リポジトリ:先ほど作成したリポジトリ
    2. ビルド構成:
      1. ブランチ^main$
      2. ビルドタイプDockerfile(ソースの場所はそのまま)
  6. 以下のように設定

    サービス名は任意の名前を付ける

    image 4.png

  7. 「コンテナ、ボリューム、ネットワーキング、セキュリティ」を開いて、「リクエスト」のリクエストのタイムアウトを3000秒に設定。これを設定しないと、ビルド時にタイムアウトしてしまう。

  8. 同じく「コンテナ、ボリューム、ネットワーキング、セキュリティ」から、「変数とシークレット」を開き、以下の変数を登録する。

    名前
    LINE_CHANNEL_ACCESS_TOKEN LINE Developersのコンソールで発行した「チャンネルアクセストークン」
    LINE_CHANNEL_SECRET LINE Official Account ManagerのMessaging APIの設定画面に書いてあった「Channnel secret」
    OPENAI_API_KEY OpenAIのAPIキー(さっき作成したやつ)
    GAS_WEBAPP_URL GASのデプロイ画面に表示されているウェブアプリURL(IDじゃないよ)
  9. 「作成」をクリックしてデプロイを開始。ビルド完了まで数分待つ

  10. デプロイが成功すると、サービスのURLが表示されるのでコピー

  11. LINE Official Account Manager のMessaging APIの設定画面の「Webhook URL」にコピーしたサービスのURLをペーストして、末尾に/callbackを付けて保存。(例: https://santa-bot-123456789123.asia-northeast1.run.app/callback

以上で準備完了です。

作成した公式アカウントが自動で自分のLINEに友達追加されていると思うので、メッセージを送ってみましょう。サンタクロースBotが返信してくれるはず!

もし数十秒待っても返信が返ってこなければ、GCPでエラーが起こっている可能性が高いので、GCPの「ログエクスプローラ」を見てみましょう。

おわりに

これでサンタクロースBotの出来上がりです。

もちろん、システムメッセージ(SANTA_INFO)を別のキャラクター設定に差し替えれば、オリジナルキャラや他の季節のイベントBotにも変身可能。ぜひいろいろ試してみてください!

Merry Christmas! そして、素敵なBot開発ライフをお楽しみください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?