【作業メモ】GoogleFormでの回答を基にレビューサイト用のメッセージを作成できるツールを作成する
目的
題名の通り
GoogleFormでの回答を基にレビューサイト用のメッセージを作成できるツールを作成する
以上が目的である
使用する技術/tool
- gemini api
- gas(Google Apps Script)
- google form
雑書きなので小文字/大文字違うの許してください👍
作業順序
- Google Formの解答欄を作成する
- GASプロジェクトの作成
- GASをGlaspを用いて、git管理できるようにする
- Gemini APIを取得
- Google Formの解答を取得し,gemini APIを用いて
いい感じのレビューを作成する仕組みを実装 - 動作確認
- 完成!
Google Formの解答欄を作成する
解答欄をつくるのすらめんどくさかったので以下のコードをAIで生成して自動で作成した
生成したコード(google form作成用コード)
function createRamenShopReviewForm() {
// フォームの新規作成
var form = FormApp.create('ラーメン屋レビューアンケート')
.setDescription('訪問したラーメン屋さんの評価をお願いします。');
// 1. 訪問したラーメン店の名前(テキスト・必須)
form.addTextItem()
.setTitle('訪問したラーメン店の名前を教えてください')
.setRequired(true);
// 2. 訪問日(日付・必須)
form.addDateItem()
.setTitle('訪問日を教えてください')
.setRequired(true);
// 3. 来店人数(数値入力)
form.addTextItem()
.setTitle('来店人数を教えてください')
.setHelpText('人数を半角数字で入力してください')
.setRequired(false)
.setValidation(FormApp.createTextValidation()
.setHelpText('数字を入力してください')
.requireNumber()
.build());
// 4. 注文したラーメンの種類(単一選択)
var ramenTypeItem = form.addMultipleChoiceItem()
.setTitle('注文したラーメンの種類を選んでください')
.setRequired(true);
ramenTypeItem.setChoices([
ramenTypeItem.createChoice('醤油ラーメン'),
ramenTypeItem.createChoice('塩ラーメン'),
ramenTypeItem.createChoice('味噌ラーメン'),
ramenTypeItem.createChoice('豚骨ラーメン'),
ramenTypeItem.createChoice('つけ麺'),
ramenTypeItem.createChoice('その他')
]);
// 5. トッピングの有無(単一選択)
var toppingPresenceItem = form.addMultipleChoiceItem()
.setTitle('トッピングはありましたか?')
.setRequired(true);
toppingPresenceItem.setChoices([
toppingPresenceItem.createChoice('あり'),
toppingPresenceItem.createChoice('なし')
]);
// 6. トッピングの種類(複数選択)※トッピングありの場合のみ表示はGoogleフォーム標準機能では非対応のため、常に表示
var toppingTypeItem = form.addCheckboxItem()
.setTitle('注文したトッピングを選んでください(複数選択可)')
.setRequired(false);
toppingTypeItem.setChoices([
toppingTypeItem.createChoice('チャーシュー'),
toppingTypeItem.createChoice('煮卵'),
toppingTypeItem.createChoice('ネギ'),
toppingTypeItem.createChoice('メンマ'),
toppingTypeItem.createChoice('のり'),
toppingTypeItem.createChoice('バター'),
toppingTypeItem.createChoice('コーン'),
toppingTypeItem.createChoice('その他')
]);
// 7. 麺の硬さ(単一選択)
var noodleFirmnessItem = form.addMultipleChoiceItem()
.setTitle('麺の硬さ')
.setRequired(true);
noodleFirmnessItem.setChoices([
noodleFirmnessItem.createChoice('バリカタ'),
noodleFirmnessItem.createChoice('カタ'),
noodleFirmnessItem.createChoice('普通'),
noodleFirmnessItem.createChoice('やわらかめ'),
noodleFirmnessItem.createChoice('その他')
]);
// 8. スープの味の濃さ(単一選択)
var soupFlavorItem = form.addMultipleChoiceItem()
.setTitle('スープの味の濃さ')
.setRequired(true);
soupFlavorItem.setChoices([
soupFlavorItem.createChoice('薄め'),
soupFlavorItem.createChoice('普通'),
soupFlavorItem.createChoice('濃いめ')
]);
// 9. 総合評価(星1~5)
var overallRatingItem = form.addMultipleChoiceItem()
.setTitle('総合評価(星1~5)を教えてください')
.setRequired(true);
overallRatingItem.setChoices([
overallRatingItem.createChoice('1'),
overallRatingItem.createChoice('2'),
overallRatingItem.createChoice('3'),
overallRatingItem.createChoice('4'),
overallRatingItem.createChoice('5')
]);
// 10. 味の満足度(1~5)
var tasteRatingItem = form.addMultipleChoiceItem()
.setTitle('味の満足度(1~5)を教えてください')
.setRequired(true);
tasteRatingItem.setChoices([
tasteRatingItem.createChoice('1'),
tasteRatingItem.createChoice('2'),
tasteRatingItem.createChoice('3'),
tasteRatingItem.createChoice('4'),
tasteRatingItem.createChoice('5')
]);
// 11. 接客対応の満足度(1~5)
var serviceRatingItem = form.addMultipleChoiceItem()
.setTitle('接客対応の満足度(1~5)を教えてください')
.setRequired(true);
serviceRatingItem.setChoices([
serviceRatingItem.createChoice('1'),
serviceRatingItem.createChoice('2'),
serviceRatingItem.createChoice('3'),
serviceRatingItem.createChoice('4'),
serviceRatingItem.createChoice('5')
]);
// 12. 店内の清潔さ(1~5)
var cleanlinessRatingItem = form.addMultipleChoiceItem()
.setTitle('店内の清潔さ(1~5)を教えてください')
.setRequired(true);
cleanlinessRatingItem.setChoices([
cleanlinessRatingItem.createChoice('1'),
cleanlinessRatingItem.createChoice('2'),
cleanlinessRatingItem.createChoice('3'),
cleanlinessRatingItem.createChoice('4'),
cleanlinessRatingItem.createChoice('5')
]);
// 13. 価格の妥当性(1~5)
var priceRatingItem = form.addMultipleChoiceItem()
.setTitle('価格の妥当性(1~5)を教えてください')
.setRequired(true);
priceRatingItem.setChoices([
priceRatingItem.createChoice('1'),
priceRatingItem.createChoice('2'),
priceRatingItem.createChoice('3'),
priceRatingItem.createChoice('4'),
priceRatingItem.createChoice('5')
]);
// 14. また来店したいと思いますか?(単一選択)
var revisitItem = form.addMultipleChoiceItem()
.setTitle('また来店したいと思いますか?')
.setRequired(true);
revisitItem.setChoices([
revisitItem.createChoice('はい'),
revisitItem.createChoice('どちらともいえない'),
revisitItem.createChoice('いいえ')
]);
// フォーム編集URLをログに出力
Logger.log('フォームが作成されました: ' + form.getEditUrl());
}
補足
ちなみに生成したGoogleFormは以下のURLより確認できる
GASプロジェクトの作成
3点リンクをクリック
Apps Scriptを選択!
作成完了!
GASをGlaspを用いて、git管理できるようにする
やっぱりナレッジ残しておくの大切やな~
既存のGASプロジェクトに接続させる
mkdir review-auto-generate
cd review-auto-generate
ファイル作ってそこに移動するいつものコマンド
npm install -g @google/clasp
claspをインストール!
clasp login
ログインをする!(詳細はワイの記事読んでくれると嬉しいな~)
clasp clone <プロジェクトid>
これを実行して以下の結果になればOKなのかな?
これで多分接続完了!
運用について
- 基本的にブラウザ上で編集した内容を都度,pullしてローカルに差分をもってきて
git+github管理を行う予定
試してみる
ブラウザ上のファイル名を変更した
clasp pull
上記コードを実行した
画像のようにpullすることで変更をローカルへ持ってこれた!
git管理について
ここはいつもやっているので省略(多分忘れないでしょ...)
Gemini APIを取得
とっても簡単だった
GASのスクリプトプロパティにAPIキーを保存(プロパティ名:GEMINI_API_KEY)
Google Formの解答を取得し,gemini APIを用いていい感じのレビュー作成処理を作成する
処理概要(AI生成)
-
フォームの回答をトリガーで検知する(onFormSubmitトリガー)
-
送信された回答データを取得する
-
取得データをGemini APIに渡してレビュー文を作成
-
レビュー内容HTMLメールを作成(コピー用ボタン+Google Mapリンク付)
-
フォームのメールアドレス収集欄で取得した回答者のメールに送信
作成コード
作成コード
// トリガー設定例(管理画面等で「フォーム送信時(onFormSubmit)」に設定してください)
function onFormSubmit(e) {
try {
// フォーム回答の回答オブジェクト取得
var response = e.response;
var itemResponses = response.getItemResponses();
// 回答内容を連想配列に格納(項目タイトル: 回答)
var answers = {};
itemResponses.forEach(function(itemResponse){
answers[itemResponse.getItem().getTitle()] = itemResponse.getResponse();
});
// 回答者メールアドレス(フォームでメールアドレス収集設定が必須)
var userEmail = e.response.getRespondentEmail();
if (!userEmail) {
Logger.log('メールアドレスが取得できません。');
return;
}
// Gemini APIに渡す形でプロンプト作成(ラーメンアンケートの項目に合わせて例示)
var promptText = buildGeminiPrompt(answers);
// Gemini APIでレビューを生成
var reviewText = callGeminiAPI(promptText);
// Google Mapリンク作成(例として「訪問したラーメン店の名前」を使う)
var shopName = encodeURIComponent(answers['訪問したラーメン店の名前を教えてください']);
var googleMapUrl = 'https://www.google.com/maps/search/?api=1&query=' + shopName;
// メール本文(HTML)作成
var htmlBody = createHtmlEmailBody(reviewText, googleMapUrl);
// メール送信
MailApp.sendEmail({
to: userEmail,
subject: 'ご訪問いただいたラーメン店のレビューをお届けします',
htmlBody: htmlBody
});
Logger.log('メール送信完了: ' + userEmail);
} catch (error) {
Logger.log('エラー: ' + error);
}
}
// Gemini APIへリクエスト送信する関数(詳細はGemini APIドキュメント参照)
function callGeminiAPI(prompt) {
// ここにGemini API呼び出しのコードを実装してください
// 以下は構造例(実際は適宜APIキーやエンドポイントを設定)
var apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
var url = 'https://api.gemini.example/v1/generate';
var payload = {
prompt: prompt,
max_tokens: 300,
temperature: 0.7
};
var options = {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': 'Bearer ' + apiKey },
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
if (json.choices && json.choices.length > 0) {
return json.choices[0].text.trim();
} else {
return 'レビュー生成失敗しました。';
}
}
// Gemini APIへ渡すプロンプトを構築(回答内容から要点を文脈化)
function buildGeminiPrompt(answers) {
return `
以下はラーメン店アンケートの回答です。これを基に、親しみやすく詳細なレビュー文を作成してください。
【店名】${answers['訪問したラーメン店の名前を教えてください']}
【訪問日】${answers['訪問日を教えてください']}
【来店人数】${answers['来店人数を教えてください']}
【注文ラーメンの種類】${answers['注文したラーメンの種類を選んでください']}
【トッピングの有無】${answers['トッピングはありましたか?']}
【トッピング種類】${answers['注文したトッピングを選んでください(複数選択可)'].join ? answers['注文したトッピングを選んでください(複数選択可)'].join(', ') : answers['注文したトッピングを選んでください(複数選択可)']}
【麺の硬さ】${answers['麺の硬さ']}
【スープの味の濃さ】${answers['スープの味の濃さ']}
【総合評価】${answers['総合評価(星1~5)を教えてください']}
【味の満足度】${answers['味の満足度(1~5)を教えてください']}
【接客対応の満足度】${answers['接客対応の満足度(1~5)を教えてください']}
【店内の清潔さ】${answers['店内の清潔さ(1~5)を教えてください']}
【価格の妥当性】${answers['価格の妥当性(1~5)を教えてください']}
【また来店したいか】${answers['また来店したいと思いますか?']}
`;
}
// HTMLメール本文作成(レビュー文・コピー用ボタン・Google Mapリンク付)
function createHtmlEmailBody(reviewText, googleMapUrl) {
return `
<html>
<body>
<h2>ご訪問いただいたラーメン店のレビュー</h2>
<pre id="reviewText" style="white-space: pre-wrap; background:#f4f4f4; padding:10px; border:1px solid #ccc;">${reviewText}</pre>
<button onclick="copyReview()" style="padding:8px 12px; margin-top:10px;">レビューをコピーする</button>
<p><a href="${googleMapUrl}" target="_blank">Google Mapでお店の場所を確認する</a></p>
<script>
function copyReview() {
const review = document.getElementById('reviewText').innerText;
navigator.clipboard.writeText(review).then(function() {
alert('レビューをコピーしました!');
}, function(err) {
alert('コピーに失敗しました: ', err);
});
}
</script>
</body>
</html>
`;
}
トリガーの設定をする
トリガーをクリックする
トリガーを追加をクリック
設定はこんな感じ
できた!!!
少しはまったことがあったのでメモする
GoogleFormへ解答してメールが来ることを期待したのだが、
うまく実装できておらず、おそらくエラーが起きているので確認したかったが
ログはGCPのログ上でしかみられない!以下の記事にてわかりやすく設定方法は説明されている
ので、そちらに任せる!
はまった点
はまった点はこのログ、GAS上でデプロイの操作した後じゃないと
ログが記録されないという点に気づかず、少し時間を無駄にしてしまった。
コード修正
コードを修正し動作するように修正した。
修正した点
- GeminiApiの扱い(エンドポイントとpayloadの形式が間違っていた)
- geminiAPIのレスポンスの受け取り方(json['candidates'][0]['content']['parts'][0]['text']で
作成されたレビューを取得)
修正後コード
// トリガー設定例(管理画面等で「フォーム送信時(onFormSubmit)」に設定してください)
function onFormSubmit(e) {
try {
// フォーム回答の回答オブジェクト取得
var response = e.response;
var itemResponses = response.getItemResponses();
// 回答内容を連想配列に格納(項目タイトル: 回答)
var answers = {};
itemResponses.forEach(function(itemResponse){
answers[itemResponse.getItem().getTitle()] = itemResponse.getResponse();
});
// 回答者メールアドレス(フォームでメールアドレス収集設定が必須)
var userEmail = e.response.getRespondentEmail();
if (!userEmail) {
Logger.log('メールアドレスが取得できません。');
return;
}
// Gemini APIに渡す形でプロンプト作成(ラーメンアンケートの項目に合わせて例示)
var promptText = buildGeminiPrompt(answers);
// Gemini APIでレビューを生成
var reviewText = callGeminiAPI(promptText);
// Google Mapリンク作成(例として「訪問したラーメン店の名前」を使う)
var shopName = encodeURIComponent(answers['訪問したラーメン店の名前を教えてください']);
var googleMapUrl = 'https://www.google.com/maps/search/?api=1&query=' + shopName;
// メール本文(HTML)作成
var htmlBody = createHtmlEmailBody(reviewText, googleMapUrl);
// メール送信
MailApp.sendEmail({
to: userEmail,
subject: 'ご訪問いただいたラーメン店のレビューをお届けします',
htmlBody: htmlBody
});
Logger.log('メール送信完了: ' + userEmail);
} catch (error) {
Logger.log('エラー: ' + error);
}
}
// Gemini APIへリクエスト送信する関数(詳細はGemini APIドキュメント参照)
function callGeminiAPI(prompt) {
var apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
var url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
var payload = {
"contents": [
{
"parts": [
{
"text": prompt
}
]
}
]
};
var options = {
method: 'post',
contentType: 'application/json',
headers: {
'X-goog-api-key': apiKey
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
// レスポンス構造はAPI仕様により変わる可能性ありますが例として
if (json['candidates'][0]['content']['parts'][0]['text']) {
return json['candidates'][0]['content']['parts'][0]['text'];
} else {
return 'レビュー生成に失敗しました。';
}
} catch (e) {
Logger.log('Gemini API呼び出しエラー: ' + e);
return 'レビュー生成エラーが発生しました。';
}
}
// Gemini APIへ渡すプロンプトを構築(回答内容から要点を文脈化)
function buildGeminiPrompt(answers) {
return `
以下はラーメン店アンケートの回答です。これを基に、親しみやすく詳細なレビュー文を作成してください。
【店名】${answers['訪問したラーメン店の名前を教えてください']}
【訪問日】${answers['訪問日を教えてください']}
【来店人数】${answers['来店人数を教えてください']}
【注文ラーメンの種類】${answers['注文したラーメンの種類を選んでください']}
【トッピングの有無】${answers['トッピングはありましたか?']}
【トッピング種類】${answers['注文したトッピングを選んでください(複数選択可)'].join ? answers['注文したトッピングを選んでください(複数選択可)'].join(', ') : answers['注文したトッピングを選んでください(複数選択可)']}
【麺の硬さ】${answers['麺の硬さ']}
【スープの味の濃さ】${answers['スープの味の濃さ']}
【総合評価】${answers['総合評価(星1~5)を教えてください']}
【味の満足度】${answers['味の満足度(1~5)を教えてください']}
【接客対応の満足度】${answers['接客対応の満足度(1~5)を教えてください']}
【店内の清潔さ】${answers['店内の清潔さ(1~5)を教えてください']}
【価格の妥当性】${answers['価格の妥当性(1~5)を教えてください']}
【また来店したいか】${answers['また来店したいと思いますか?']}
`;
}
// HTMLメール本文作成(レビュー文・コピー用ボタン・Google Mapリンク付)
function createHtmlEmailBody(reviewText, googleMapUrl) {
return `
<html>
<body>
<h2>ご訪問いただいたラーメン店のレビュー</h2>
<pre id="reviewText" style="white-space: pre-wrap; background:#f4f4f4; padding:10px; border:1px solid #ccc;">${reviewText}</pre>
<button onclick="copyReview()" style="padding:8px 12px; margin-top:10px;">レビューをコピーする</button>
<p><a href="${googleMapUrl}" target="_blank">Google Mapでお店の場所を確認する</a></p>
</body>
</html>
`;
}
動作確認
事前入力機能を用いて、以下の事前入力済みGoogleFormを利用して楽にテストを行った!
参考
テスト結果
以上のようにレビューを作成しメールアドレスに送信することができた!
追加で考えること(時間があったらやってみるか)
-
レビューの質を上げる!
-
HTMLメールのCSSの質を上げる!
現在のコードだとシンプルなプロンプトかつファインチューニングとか
小難しい技術をつかわず、やっているので全くレビューの質が良くないので
なんとか色々試して、レビューの質を上げたい(まずは、レビューの文字数が長すぎるので
それを制御させることから始めたい)今全くCSSを付けていないのでstyleタグまたは,tailwindCSSなどをcdn経由で読み込んで
おしゃれなメールにする必要がありそう!
まぁ時間があったらやる程度で緩くやっていきます!
とりあえずコア機能完成!!!
お疲れ様でした!
↓完成したコード