はじめに
本シリーズでは、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を作成していない場合は、必ずこちらを参考にして作成してください。
今回やること
- 入力テキストと出力テキストの保存
- 保存したテキストにより会話の継続性を実現
STEP.1 入力テキストと出力テキストの保存
GASには短時間のデータ保存に役立つCacheService
があります。今回はこれを活用して、LINEに入力した文字列およびGemini Proからの回答文字列を保存し、会話に利用します。
CacheService
は以下のように3種類ありますが、今回はScriptCache
を使用します。
メソッド | 説明 |
---|---|
getDocumentCache() | 現在のドキュメントとスクリプトをスコープとするキャッシュインスタンスを取得します。 |
getScriptCache() | スクリプトをスコープとするキャッシュインスタンスを取得します。 |
getUserCache() | 現在のユーザーとスクリプトをスコープとするキャッシュインスタンスを取得します。 |
+ 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
ごとの会話を設定することで、会話の継続性を実現できるということです。
実装
それではやってみましょう。
/**
* 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の回答文字列内の特殊文字や改行をエスケープするためです。
プログラム全文
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
のキーはuser
とmodel
と固定文字です。そのため、もしこの相談Botを複数人で使いたいという場合には、キーに対する値は他人の入出力により更新されてしまいます。これでは会話の継続性がまた失われてしまいますよね。
そういった場合は、以下のようにLINEのuserIdをprefixに設定したりすれば解決できます。
sCache.put(eventData.source.userId+'user', uText.slice(0, 10000));
sCache.put(eventData.source.userId+'model', gemini.slice(0, 10000));
どうでしょう、もうGASはなんでもできる! は疑いようがないですね!!!引き続き、GASを楽しんでいきましょう!!
記事を読んで、「良いな」や「今後に期待できる!」と感じて頂けたらいいねやフォロー、コメントいただけると幸いです。それではまた次回をお楽しみに!