Closed12

【作業メモ】GoogleFormでの回答を基にレビューサイト用のメッセージを作成できるツールを作成する

ガネーシャガネーシャ

目的

題名の通り

GoogleFormでの回答を基にレビューサイト用のメッセージを作成できるツールを作成する

以上が目的である

使用する技術/tool

  • gemini api
  • gas(Google Apps Script)
  • google form
    雑書きなので小文字/大文字違うの許してください👍
ガネーシャガネーシャ

作業順序

  1. Google Formの解答欄を作成する
  2. GASプロジェクトの作成
  3. GASをGlaspを用いて、git管理できるようにする
  4. Gemini APIを取得
  5. Google Formの解答を取得し,gemini APIを用いて
    いい感じのレビューを作成する仕組みを実装
  6. 動作確認
  7. 完成!
ガネーシャガネーシャ

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より確認できる
https://forms.gle/YYuRwrFork1bLWh66

ガネーシャガネーシャ

GASプロジェクトの作成


3点リンクをクリック

Apps Scriptを選択!

作成完了!

ガネーシャガネーシャ

GASをGlaspを用いて、git管理できるようにする

https://zenn.dev/ganesya/articles/93bfda0e49db22
やり方を忘れていたが、上記の自分の記事に助けられそう
やっぱりナレッジ残しておくの大切やな~

既存の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管理について

ここはいつもやっているので省略(多分忘れないでしょ...)

ガネーシャガネーシャ

Google Formの解答を取得し,gemini APIを用いていい感じのレビュー作成処理を作成する

処理概要(AI生成)

  1. フォームの回答をトリガーで検知する(onFormSubmitトリガー)

  2. 送信された回答データを取得する

  3. 取得データをGemini APIに渡してレビュー文を作成

  4. レビュー内容HTMLメールを作成(コピー用ボタン+Google Mapリンク付)

  5. フォームのメールアドレス収集欄で取得した回答者のメールに送信

作成コード

作成コード

// トリガー設定例(管理画面等で「フォーム送信時(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のログ上でしかみられない!以下の記事にてわかりやすく設定方法は説明されている
ので、そちらに任せる!
https://qiita.com/gas-suke/items/4a760921913c06e0ae9f

はまった点

はまった点はこのログ、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>
  `;
}

ガネーシャガネーシャ

追加で考えること(時間があったらやってみるか)

  • レビューの質を上げる!

  • HTMLメールのCSSの質を上げる!

    レビューの質を上げる

    現在のコードだとシンプルなプロンプトかつファインチューニングとか
    小難しい技術をつかわず、やっているので全くレビューの質が良くないので
    なんとか色々試して、レビューの質を上げたい(まずは、レビューの文字数が長すぎるので
    それを制御させることから始めたい)

    HTMLメールのCSSの質を上げる

    今全くCSSを付けていないのでstyleタグまたは,tailwindCSSなどをcdn経由で読み込んで
    おしゃれなメールにする必要がありそう!
    まぁ時間があったらやる程度で緩くやっていきます!

このスクラップは1ヶ月前にクローズされました