0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASでフォーム回答からAIフィードバック下書きを作る続編: 校務で使う再現手順とコード例

0
Posted at

この記事では、Googleフォームの回答を起点に、Google Apps Scriptで個別フィードバックの下書きを作る構成を、続編としてもう少し再現しやすい形に整理します。

前回の「フォーム回答から個別FB下書き」系の流れを、校務で扱いやすいように次の3段階に分けます。

  1. フォーム回答をスプレッドシートに集める
  2. GASで回答行を読み取り、AIに渡すプロンプトを組み立てる
  3. 生成結果を「下書き」列に書き戻し、最終確認しやすくする

AIのAPIキーはフロントエンドに置かず、GAS側のスクリプトプロパティに保存してサーバーサイドから呼び出します。コード内にも記事本文にも実キーは書きません。

想定するシート

フォーム回答先のスプレッドシートに、次の列がある想定です。

内容
A タイムスタンプ
B 氏名または識別名
C 提出内容
D 自己評価
E フィードバック下書き
F 生成日時

E列とF列は、あとから手動で追加しておきます。

事前準備

  1. Googleフォームを作成する
  2. 回答先をGoogleスプレッドシートにする
  3. スプレッドシートで 拡張機能 > Apps Script を開く
  4. Apps Scriptのプロジェクト設定からスクリプトプロパティに AI_API_KEY を保存する

APIキーをソースコードに直接書かないため、PropertiesService.getScriptProperties() で読み出します。

コード例

以下は、未生成の行だけを処理して、E列にフィードバック下書き、F列に生成日時を書き込む例です。

const SHEET_NAME = 'フォームの回答 1';
const COL_NAME = 2;
const COL_SUBMISSION = 3;
const COL_SELF_REVIEW = 4;
const COL_DRAFT = 5;
const COL_GENERATED_AT = 6;

function generateFeedbackDrafts() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`Sheet not found: ${SHEET_NAME}`);
  }

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return;

  const values = sheet.getRange(2, 1, lastRow - 1, COL_GENERATED_AT).getValues();

  values.forEach((row, index) => {
    const rowNumber = index + 2;
    const name = row[COL_NAME - 1];
    const submission = row[COL_SUBMISSION - 1];
    const selfReview = row[COL_SELF_REVIEW - 1];
    const existingDraft = row[COL_DRAFT - 1];

    if (!submission || existingDraft) return;

    const prompt = buildFeedbackPrompt({
      name,
      submission,
      selfReview,
    });

    const draft = callAiApi(prompt);

    sheet.getRange(rowNumber, COL_DRAFT).setValue(draft);
    sheet.getRange(rowNumber, COL_GENERATED_AT).setValue(new Date());
  });
}

function buildFeedbackPrompt({ name, submission, selfReview }) {
  return [
    'あなたは学校の提出物に対して、学習者が次の改善に移りやすいフィードバック下書きを作る補助者です。',
    '断定しすぎず、提出内容に基づいて具体的に書いてください。',
    '出力は次の3項目です。',
    '1. 良かった点',
    '2. 次に伸ばせそうな点',
    '3. 具体的な次の一歩',
    '',
    `氏名または識別名: ${name || '未入力'}`,
    `提出内容: ${submission}`,
    `自己評価: ${selfReview || '未入力'}`,
  ].join('\n');
}

function callAiApi(prompt) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('AI_API_KEY');
  if (!apiKey) {
    throw new Error('AI_API_KEY is not set in script properties.');
  }

  const response = UrlFetchApp.fetch('https://api.openai.com/v1/responses', {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: `Bearer ${apiKey}`,
    },
    payload: JSON.stringify({
      model: 'gpt-4.1-mini',
      input: prompt,
    }),
    muteHttpExceptions: true,
  });

  const status = response.getResponseCode();
  const body = response.getContentText();

  if (status < 200 || status >= 300) {
    throw new Error(`AI API request failed: ${status} ${body}`);
  }

  const json = JSON.parse(body);
  return extractText(json);
}

function extractText(responseJson) {
  const output = responseJson.output || [];
  const texts = [];

  output.forEach((item) => {
    (item.content || []).forEach((content) => {
      if (content.type === 'output_text' && content.text) {
        texts.push(content.text);
      }
    });
  });

  return texts.join('\n').trim();
}

実行手順

  1. Apps Scriptに上のコードを貼る
  2. SHEET_NAME が実際の回答シート名と一致しているか確認する
  3. スクリプトプロパティに AI_API_KEY を保存する
  4. generateFeedbackDrafts を手動実行して権限を許可する
  5. E列に下書き、F列に生成日時が入ることを確認する

初回は手動実行で動作を確認し、意図した下書きになってから時間主導トリガーやフォーム送信トリガーに進めると扱いやすくなります。

フォーム送信時に動かす場合

フォーム送信のたびに生成したい場合は、次の関数を追加します。

function onFormSubmit(e) {
  const sheet = e.range.getSheet();
  if (sheet.getName() !== SHEET_NAME) return;

  const rowNumber = e.range.getRow();
  const row = sheet.getRange(rowNumber, 1, 1, COL_GENERATED_AT).getValues()[0];

  const name = row[COL_NAME - 1];
  const submission = row[COL_SUBMISSION - 1];
  const selfReview = row[COL_SELF_REVIEW - 1];
  const existingDraft = row[COL_DRAFT - 1];

  if (!submission || existingDraft) return;

  const prompt = buildFeedbackPrompt({
    name,
    submission,
    selfReview,
  });

  const draft = callAiApi(prompt);

  sheet.getRange(rowNumber, COL_DRAFT).setValue(draft);
  sheet.getRange(rowNumber, COL_GENERATED_AT).setValue(new Date());
}

Apps Scriptのトリガー画面で、onFormSubmit を「フォーム送信時」に設定します。

校務で使うときの調整ポイント

校務で使うAI活用tipsとしては、AIに任せる範囲を「最終コメント」ではなく「下書き」に限定しておくと運用しやすくなります。

プロンプトでは、次のように出力形式を固定しておくと、確認時の目線をそろえやすくなります。

出力は次の3項目です。
1. 良かった点
2. 次に伸ばせそうな点
3. 具体的な次の一歩

また、フォーム項目が増えた場合は、列番号の定数を増やして buildFeedbackPrompt() に渡す情報を追加します。

const COL_GOAL = 7;

// 例:
const goal = row[COL_GOAL - 1];

この形にしておくと、フォーム、スプレッドシート、GASの役割が分かれます。フォームは入力、スプレッドシートは確認場所、GASは下書き生成の処理として扱えます。

まとめ

GASでフォーム回答を読み取り、AIで個別フィードバック下書きを作り、スプレッドシートへ戻す流れを作ると、校務で使うAI活用の手順をコード込みで共有できます。

大事なのは、APIキーをコードに書かないこと、AIの出力をそのまま最終文にしないこと、そして再現できる小さな構成から始めることです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?