4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事投稿キャンペーン 「2024年!初アウトプットをしよう」

【0からGASを学ぶ】GAS × Gemini Pro API × LINE Messaging API の相談Botで会話しよう

Last updated at Posted at 2023-12-31

はじめに

本シリーズでは、GASの始め方や便利な使い方、ビジネス活用まで幅広く解説します。シリーズをひと通り読んでいただければ、あなたもきっとGASマスターになれるはずです。

シリーズの対象者

  • そもそもGASってなんだかわからない
  • GASを学びたいけど何から始めればいいかわからない方
  • GASはわかり始めたけど、もっと活用ができないかと模索している方
  • とにかくGoogleが好き! という方

前回記事

Gemini Pro API をGASからアクセスする

では早速始めていきましょう。【0からGASを学ぶ】シリーズの第16回は「GAS × Gemini Pro API × LINE Messaging API の相談Botで会話しよう」です。前回は、GAS × Gemini Pro API × LINE Messaging APIにより相談Botを作成するまでを行いました。しかしながら、記事の末尾に記載した通り、相談Botでありながら会話の継続性が実現できておらず、今一つの印象だったかと思います。そこで、今回は前回作成した相談Botをもう一段階進化させましょう。

前回作成した相談Botを前提に記事を書いておりますので、まだ相談Botを作成していない場合は、必ずこちらを参考にして作成してください。

今回やること

  1. 入力テキストと出力テキストの保存
  2. 保存したテキストにより会話の継続性を実現

STEP.1 入力テキストと出力テキストの保存

GASには短時間のデータ保存に役立つCacheServiceがあります。今回はこれを活用して、LINEに入力した文字列およびGemini Proからの回答文字列を保存し、会話に利用します。
CacheServiceは以下のように3種類ありますが、今回はScriptCacheを使用します。

メソッド 説明
getDocumentCache() 現在のドキュメントとスクリプトをスコープとするキャッシュインスタンスを取得します。
getScriptCache() スクリプトをスコープとするキャッシュインスタンスを取得します。
getUserCache() 現在のユーザーとスクリプトをスコープとするキャッシュインスタンスを取得します。

doPostの改修
+ const sCache = CacheService.getScriptCache();
/**
 * LINEのトークでメッセージが送信された際に起動するメソッド
 * @param {EventObject} e - イベントオブジェクト
 */
function doPost(e){
  // イベントデータはJSON形式となっているため、parseして取得
  const eventData = JSON.parse(e.postData.contents).events[0]
        , repToken = eventData.replyToken
        , msgType = eventData.message.type;
  // テキストメッセージのときのみ
  if (msgType=='text') {
    let uText = eventData.message.text
        , gemini = getGeminiProAnswerTxt(uText);
    replyTxt(repToken, gemini);
+   sCache.put('user', uText.slice(0, 10000));
+   sCache.put('model', gemini.slice(0, 10000));
  }
}

上記のようにScriptCacheを使用するための宣言を行い、入力テキストと出力テキストを保存します。

Cacheにはキーごとに保存できる最大量が100KB(日本語だとおよそ50,000文字程度) であるため、ひとまず10,000文字程度に切り出しをしています。
また、デフォルトでは600秒の有効期限となっています。最大6時間(21,600秒) まで保存できるため、もし会話の継続性をながーーーくしたければ、
sCache.put('user', uText.slice(0, 10000), 21600);
とすることで長時間保存が可能です。

STEP.2 保存したテキストにより会話の継続性を実現

仕様の確認

改めて、Gemini Pro APIの仕様を確認しましょう。

こちらで確認できるようroleを指定し、複数のパラメタを設定することでMulti-turn conversations (chat)を実現できることがわかります。

curl https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=$API_KEY \
    -H 'Content-Type: application/json' \
    -X POST \
    -d '{
      "contents": [
        {"role":"user",
         "parts":[{
           "text": "Write the first line of a story about a magic backpack."}]},
        {"role": "model",
         "parts":[{
           "text": "In the bustling city of Meadow brook, lived a young girl named Sophie. She was a bright and curious soul with an imaginative mind."}]},
        {"role": "user",
         "parts":[{
           "text": "Can you set it in a quiet village in 1600s France?"}]},
      ]
    }' 2> /dev/null | grep "text"

つまり、これまではcontentsには'parts' > 'text'のみの設定でしたが、そこにroleごとの会話を設定することで、会話の継続性を実現できるということです。

実装

それではやってみましょう。

getGeminiProAnswerTxtの改修
/**
 * LINEのトークに送信されたメッセージをGemini Pro APIに渡して回答を得るメソッド
 * @param {String} txt - 送信されたメッセージ
 */
function getGeminiProAnswerTxt(txt) {
+ let contentsStr = '';
+ // キャッシュにuidに紐づく情報が存在した場合、情報には過去の質問文が入っているためそれを取得
+ if (sCache.get('user')) {
+   contentsStr += `{
+     "role": "user",
+     "parts": [{ 
+       "text": ${JSON.stringify(sCache.get('user'))}
+     }]
+   },
+   {
+     "role": "model",
+     "parts": [{
+       "text": ${JSON.stringify(sCache.get('model'))}
+     }]
+   },`
+ }
+ contentsStr += `{
+   "role": "user",
+   "parts": [{
+     "text": ${JSON.stringify(txt)}
+   }]
+ }`
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${GEMINI_API}`
        , payload = {
-           'contents': [
-             {
-               'parts': [{
-                 'text': txt
-               }]
-             }
-           ]
+           'contents': JSON.parse(`[${contentsStr}]`)
          }
        , options = {
            'method': 'post',
            'contentType': 'application/json',
            'payload': JSON.stringify(payload)
          };
  const res = UrlFetchApp.fetch(url, options)
        , resJson = JSON.parse(res.getContentText());

  if (resJson && resJson.candidates && resJson.candidates.length > 0) {
    return resJson.candidates[0].content.parts[0].text;
  } else {
    return '回答を取得できませんでした。';
  }
}

このようにすることでsCache.get('user')がある、つまりは前回の入力テキストがある場合は、入力テキストをrole:user出力テキストをrole:model に設定したうえで、今回の入力テキストをrole:userに設定し、Gemini Pro APIに依頼を出せるようになります。
ただし、上記のやり方はあくまでも1回前の入出力を設定するだけです。そのため、複数回にわたるトーク履歴を設定したいのであれば、それをScriptCacheに保存し、role:userおよびrole:modelを構築すればばっちりです。ただし、1回前の入出力だけでも大抵の場合は会話が成り立ちます。会話というのは単純なものですね笑

${JSON.stringify(sCache.get('model'))}としているのは、入力文字列やGeminiの回答文字列内の特殊文字や改行をエスケープするためです。

プログラム全文

Qiita016
const GEMINI_API    = '**第14回記事のSTEP.1のNo.5で取得したAPI keyを記載する**';
const REPLY_URL     = 'https://api.line.me/v2/bot/message/reply';
const LINEAPI_TOKEN = '**第15回記事のSTEP.1のNo.17で取得したチャネルアクセストークンを記載する**';

const sCache = CacheService.getScriptCache();
/**
 * LINEのトークでメッセージが送信された際に起動するメソッド
 * @param {EventObject} e - イベントオブジェクト
 */
function doPost(e){
  // イベントデータはJSON形式となっているため、parseして取得
  const eventData = JSON.parse(e.postData.contents).events[0]
        , repToken = eventData.replyToken
        , msgType = eventData.message.type;
  // テキストメッセージのときのみ
  if (msgType=='text') {
    let uText = eventData.message.text
        , gemini = getGeminiProAnswerTxt(uText);
    replyTxt(repToken, gemini);
    sCache.put('user', uText.slice(0, 10000));
    sCache.put('model', gemini.slice(0, 10000));
  }
}

/**
 * LINEのトークに送信されたメッセージをGemini Pro APIに渡して回答を得るメソッド
 * @param {String} txt - 送信されたメッセージ
 */
function getGeminiProAnswerTxt(txt) {
  let contentsStr = '';
  // キャッシュにuidに紐づく情報が存在した場合、情報には過去の質問文が入っているためそれを取得
  if (sCache.get('user')) {
    contentsStr += `{
      "role": "user",
      "parts": [{
        "text": ${JSON.stringify(sCache.get('user'))}
      }]
    },
    {
      "role": "model",
      "parts": [{
        "text": ${JSON.stringify(sCache.get('model'))}
      }]
    },`
  }
  contentsStr += `{
    "role": "user",
    "parts": [{
      "text": ${JSON.stringify(txt)}
    }]
  }`
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${GEMINI_API}`
        , payload = {
            'contents': JSON.parse(`[${contentsStr}]`)
          }
        , options = {
            'method': 'post',
            'contentType': 'application/json',
            'payload': JSON.stringify(payload)
          };
  const res = UrlFetchApp.fetch(url, options)
        , resJson = JSON.parse(res.getContentText());

  if (resJson && resJson.candidates && resJson.candidates.length > 0) {
    return resJson.candidates[0].content.parts[0].text;
  } else {
    return '回答を取得できませんでした。';
  }
}

/**
 * LINEのトークにメッセージを返却するメソッド
 * @param {String} token - メッセージ返却用のtoken
 * @param {String} text - 返却テキスト
 */
function replyTxt(token, txt){
  const message = {
                    'replyToken' : token,
                    'messages' : [{
                      'type': 'text',
                      'text': txt
                    }]
                  }
        , options = {
                    'method' : 'post',
                    'headers' : {
                      'Content-Type': 'application/json; charset=UTF-8',
                      'Authorization': 'Bearer ' + LINEAPI_TOKEN,
                    },
                    'payload' : JSON.stringify(message)
                  };
  UrlFetchApp.fetch(REPLY_URL, options);
}

プログラムの修正が終わったら、再デプロイしましょう。デプロイ方法を誤るとデプロイされたURLが変わってしまうので、必ずこちらを参考に行ってください。
※URLが変わるとLINE Developer側の設定も変更しなければなりません。

確認

…は、ぜひお手持ちのスマホで実施してみてください。お楽しみに!

おわりに

お疲れ様でした。
第16回は「GAS × Gemini Pro API × LINE Messaging API の相談Botで会話しよう」ということで、第15回で作成した相談Botをさらに進化させました。
ですが、これはあくまでも"あなただけの"相談Botなので、ScriptCacheのキーはusermodelと固定文字です。そのため、もしこの相談Botを複数人で使いたいという場合には、キーに対する値は他人の入出力により更新されてしまいます。これでは会話の継続性がまた失われてしまいますよね。
そういった場合は、以下のようにLINEのuserIdをprefixに設定したりすれば解決できます。

uidの活用
sCache.put(eventData.source.userId+'user', uText.slice(0, 10000));
sCache.put(eventData.source.userId+'model', gemini.slice(0, 10000));

どうでしょう、もうGASはなんでもできる! は疑いようがないですね!!!引き続き、GASを楽しんでいきましょう!!
記事を読んで、「良いな」や「今後に期待できる!」と感じて頂けたらいいねフォローコメントいただけると幸いです。それではまた次回をお楽しみに!

ブログでより詳しく解説しています!

以下画像をクリックしてブログにアクセス!!

4
7
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
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?