【GAS】Googleの生成AI「Gemini Pro API 」を使って会話ができるLINE botを作成する!(中編)

2023年12月13日より、開発者や企業は、Googleの生成AI「Gemini API 」を介して Gemini Pro にアクセスできるようになりました

こーすけ先生

3つのモデルのうち、2023年12月13日から「Gemini Pro」だけ使えるようになっています!「Google×生成AI」これは使うしかないでしょ!

また、2023年12月19日から、Bard でGoogle のアプリやサービス(Gmail、Google ドキュメント、Google ドライブ、Google マップ等…)と連携できる拡張機能が日本語版でも公開されました

今後、「Google×AI」の連携がドンドン増えていくこと間違いなしです!

本記事の内容
  • Googleの生成AI「Gemini Pro」を使ってオリジナルのLINE botを作る(前回)
  • 前回作ったLINE botを基に会話が継続できるよう改良する
こーすけ先生

前回の記事で作ったLINE botを会話ができるようもう一段階レベルアップさせてみましょう!
この機会に、皆さんも話題の「Gemini Pro API」を使ってみてね!

この記事を読めば、会話ができるオリジナルLINE botが完成します!
かなりワクワクする内容だと思うので、是非読んでいってください!

この記事の執筆者について
  • GASの人
  • ITベンダSEとして12年勤務する中で民間、金融、官公庁の現場を一通り経験済
  • 現在は公務員をやりながら起業に向けて着々と準備中
GASなら任せろ!

​GASを極めたい方や、業務の効率化を図りたい方は、ぜひこの記事を読んください!
難しいことはGASに任せて、我々人間は楽しちゃいましょう!

こーすけ先生

当ブログでは実際に仕事でGASを扱っている私が、GASの魅力について徹底的に取り上げていきます!

目次

前回作った生成AI「Gemini Pro」を使ったオリジナルのLINE botを進化させよう!

本記事で取り組む演習
  • 会話を実現するために入力テキストと出力テキストの保存方法を学ぶ
  • 保存したテキストにより会話の継続性を実現する
こーすけ先生

完成形のイメージはこんなカンジ!
前回作ったものからレベルアップして会話ができるLINE botの完成を目指します!

こーすけ先生

【前編】まで終えてから本記事を読んでね!

【STEP1】会話を実現するために入力テキストと出力テキストを保存する

GASには短時間のデータ保存に役立つCacheServiceがあります

こーすけ先生

今回はこれを活用して、LINEに入力した文字列およびGemini Proからの回答文字列を保存し、会話に利用します

CacheServiceは以下のように3種類ありますが、今回はScriptCacheを使用します

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

getScriptCache()は、Google Apps Script内でスクリプト全体のスコープで使えるキャッシュを取得するメソッドです

こーすけ先生

「スクリプト全体のスコープ」とは、同じスクリプト内の異なる関数や処理の間でデータを共有できるという意味です

通常、関数内で変数を宣言すると、その変数はその関数内でのみ有効で、他の関数では利用できません
しかし、getScriptCache()を使うと、スクリプト全体で使えるキャッシュを取得できます

このキャッシュを使うことで、スクリプト内の異なる場所でデータを共有できるようになります

 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);
とすることで長時間保存が可能です。

【STEP2】保存したテキストにより会話の継続性を実現

こーすけ先生

改めて、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ごとの会話を設定することで、会話の継続性を実現できるということです

こーすけ先生

それでは実装していきましょう!

/**
 * 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 '回答を取得できませんでした。';
  }
}
こーすけ先生

過去のユーザーの入力テキスト (user) と、Gemini Pro の前回の応答テキスト (model) を ScriptCacheから取得します

このようにすることで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の回答文字列内の特殊文字や改行をエスケープするためです

JSON.stringify()メソッドは、JavaScriptオブジェクトをJSON形式の文字列に変換します
これにより、データをサーバーに送信する前にデータを整形することができます

こーすけ先生

これは、文字列の中に変数やオブジェクトを埋め込む際に便利な機能です

【STEP3】再デプロイする

const GEMINI_API      = '**「Gemini Pro」のAPI keyを記載する**'
      , REPLY_URL     = 'https://api.line.me/v2/bot/message/reply'
      , LINEAPI_TOKEN = '**取得したLINEのチャネルアクセストークンを記載する**';

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側の設定も変更しなければなりません)

STEP
「デプロイを管理」をクリック
STEP
デプロイ管理ダイアログが表示されるため、やや右上の鉛筆マークをクリック
STEP
[バージョン]に新バージョンを選択し「デプロイ」をクリック

これで完成です!お疲れ様でした!

(補足)相談Botを複数人で使いたい場合

これはあくまでも”あなただけの“相談Botなので、ScriptCacheのキーはusermodelと固定文字です

こーすけ先生

そのため、もしこの相談Botを複数人で使いたいという場合には、キーに対する値は他人の入出力により更新されてしまいます
これでは会話の継続性がまた失われてしまいますよね

そういった場合は、以下のようにLINEのuserIdをprefixに設定したりすれば解決できます。

sCache.put(eventData.source.userId+'user', uText.slice(0, 10000));
sCache.put(eventData.source.userId+'model', gemini.slice(0, 10000));
こーすけ先生

私が作った「LINE bot」です!
良かったらお友だちになってください!

まとめ

今回は「前回作ったLINE botに会話を継続できるようバージョンアップしよう!」というテーマで解説を行いました

こーすけ先生

どうでしょう、もうGASはなんでもできる! は疑いようがないですね!!!

ぜひ、皆さんも実業務での活用に取り組んでみてください
引き続き、GASを楽しんでいきましょう!!

…ところで「Gemini」はマルチモーダル式で作成されていて、画像の理解に長けている生成AIです

こーすけ先生

LINE上でも「Gemini」に「画像」を認識させたくない?

がすぴょん

させたい!
させたい!

というワケで後編は、LINE bot上で「画像」も扱えるように設定します!

こーすけ先生

GAS×LINE APIだとログが出力されない!と質問を受けたので、外部サービスと連携する際にログを出力する方法についてまとめました!

こーすけ先生

X(旧:Twieer)にて、ブログの更新やQiita記事の更新、GAS情報をお届けしますので、是非フォローしてください!

100日後に起業する公務員

こーすけ先生

退職までの漫画をゆるくXにて更新中!
是非フォローしてね!

この記事が気に入ったら
いいね または フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

GASの人。ITベンダSEとして12年勤務し、民間、金融、官公庁の現場を一通り経験済。html、css、JavaScript、Java、PHPも分かります。最近は専らGASで小規模アプリケーションを頻繁に作成しています。GASのことなら何でもお任せあれ!現在は公務員として働きながら、起業に向けて着々と準備中です!

コメント

コメントする

CAPTCHA


目次