スプレッドシートとGASでシフト表を自動割り当て!ChatGPTを活用した業務効率化

スプレッドシートでシフト表を作成する際、予想以上に時間がかかってしまうことありませんか?

シフトのバランスを考えたり、同じ従業員が連続して同じシフトに入らないよう調整するのも簡単ではありませんよね。

そんな時に役立つのが、AIを活用したシフト割り当ての自動化です。

この記事では、ChatGPTとGoogleスプレッドシートを組み合わせて、シフト表の割り当てを自動化する方法をご紹介します。

これまで手間のかかっていたシフト表作成も効率化できます。

なお、スプレッドシートとChatGPTを組み合わせるには ChatGPT APIキーが必要になります。

APIキーの取得方法については、こちらの記事でご紹介しています。

目次

完成イメージ

ChatGPTへの指示内容(概略)
  • 各日には「早番」が3名、「遅番」が3名それ以外は「休み」として割り当てること
  • 同じ従業員が連続して「早番」や「遅番」にならないようにすること
  • シフトができるだけ均等に割り当てられるようにすること

今回の例は簡易的なシフト表として、「早番」、「遅番」、「休み」の3つをランダムに入力としています。

ChatGPTへの条件を追加することで、従業員のスキルや希望日時を考慮したシフト表の作成も可能です。

手順

STEP
スプレッドシートを準備

スプレッドシートには、従業員リストやシフト日付の情報を入力します。

例えば、従業員名をA列に、シフトの日付をB列以降に設定します。

項目の入力(例)

入力内容
A1
A2
B6以降従業員名
B4:AF4日付
B5:AF5曜日

なお、今回の例ではシート名を「シフト表」としてGAS実行の際に参照します。

STEP
GASエディタを開く
画像に alt 属性が指定されていません。ファイル名: GAS.png

Googleスプレッドシートを開き、メニューの「拡張機能」から「Apps Script」をクリックしてGASエディタを開きます。

APIキーの取得についてはこちらの記事をご参照ください。

STEP
GASスクリプトの作成

function myFunction(){

}

が最初から入っているため、消去して以下のスクリプトを貼り付けます。

const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');

function assignShiftsWithChatGPT() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シフト表");
  
  // A6以降の従業員名を取得
  const members = sheet.getRange("A6:A" + sheet.getLastRow()).getValues()
    .flat()
    .filter(name => name);

  // B4:AF4の範囲から日付リストを取得
  const dates = sheet.getRange("B4:AF4").getValues()[0].filter(date => date);

  // プロンプトを生成
  const prompt = createShiftPrompt(members, dates);

  // ChatGPT APIにリクエストを送信
  const chatGPTResponse = callChatGPTAPI(prompt);

  // ChatGPTからの応答をシフト表に反映
  if (chatGPTResponse) {
    applyShiftAssignments(sheet, chatGPTResponse, 2, 6); // 日付の開始列(B列=2)と従業員の開始行(6行目)
  }
}

function createShiftPrompt(members, dates) {
  return `以下の従業員リストと日付に基づいて、シフト表を厳密にJSON形式でのみ出力してください。以下の指示を厳密に守り、余分な説明を含めないでください。

` +
         `条件:
- 各日には必ず「早番」が3名、「遅番」が3名、それ以外の従業員は「休み」として割り当ててください。
` +
         `- シフトは以下の3種類です:「早」(早番)、「遅」(遅番)、「休」(休み)。
` +
         `- 各従業員は、できるだけ公平に「早」「遅」「休」のシフトが割り当てられるようにしてください。
` +
         `- 同じ従業員が連続して「早」または「遅」シフトに入らないようにしてください。
` +
         `- 全員のシフトが均等になるようにし、できるだけランダムに割り当ててください。
` +
         `- 各日付のシフトには、毎回異なる従業員が含まれるようにしてください。
` +
         `- 各従業員が各シフト(「早」、「遅」、「休」)をできるだけ均等に受けるようにしてください。

` +
         `出力は必ず有効なJSON形式で次のようにしてください:
` +
         `{
  "10/1": {"従業員名1": "早", "従業員名2": "早", "従業員名3": "早", "従業員名4": "遅", "従業員名5": "遅", "従業員名6": "遅", "従業員名7": "休", "従業員名8": "休"},
  "10/2": {"従業員名1": "遅", "従業員名2": "早", "従業員名3": "早", "従業員名4": "早", "従業員名5": "遅", "従業員名6": "遅", "従業員名7": "休", "従業員名8": "休"}
}` +
         `

従業員リスト: ${members.join(", ")}
` +
         `日付リスト: ${dates.join(",")}

` +
         `注意: 必ず出力は厳密にJSON形式で出力してください。余分な説明は一切含めないでください。途中で切れることなく完全なJSONを返してください。`;
}

function callChatGPTAPI(prompt) {
  const url = 'https://api.openai.com/v1/chat/completions';
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': `Bearer ${OPENAI_API_KEY}`
    },
    payload: JSON.stringify({
      model: 'gpt-4o',
      messages: [
        { role: 'system', content: 'You are an AI that only outputs valid JSON based on user prompts.' },
        { role: 'user', content: prompt }
      ],
      max_tokens: 4500,
      temperature: 0.7
    })
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const json = JSON.parse(response.getContentText());

    // ChatGPTの応答を取得
    let content = json.choices[0].message.content.trim();

    // JSON形式の部分のみを抽出する処理
    const jsonStart = content.indexOf('{');
    const jsonEnd = content.lastIndexOf('}') + 1;
    if (jsonStart !== -1 && jsonEnd !== -1) {
      content = content.slice(jsonStart, jsonEnd);
    }

    // JSONパースの試行
    try {
      return JSON.parse(content); // JSONとしてパースを試みる
    } catch (e) {
      Logger.log("Error: The response is not valid JSON.");
      Logger.log("Received content: " + content);
      Logger.log("Prompt used: " + prompt);
      return null; // JSONとしてパースできない場合
    }
  } catch (e) {
    Logger.log("Error: " + e.toString());
    return null;
  }
}

function applyShiftAssignments(sheet, shiftAssignments, startDateColumn, startRow) {
  // ChatGPTの応答をシフト表に反映
  Object.keys(shiftAssignments).forEach((date, dateIndex) => {
    const col = startDateColumn + dateIndex;
    const assignments = shiftAssignments[date];
    let rowIndex = startRow;

    // 各従業員のシフトを対応するセルに配置
    Object.keys(assignments).forEach((member) => {
      const shift = assignments[member];
      sheet.getRange(rowIndex, col).setValue(shift);
      rowIndex++;
    });
  });
}
スクリプトの説明
APIキーの取得
const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
  • PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY')を使って、スクリプトプロパティ内に保存されたOPENAI_API_KEYという名前のAPIキーを取得します。
assignShiftsWithChatGPT関数
function assignShiftsWithChatGPT() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シフト表");
  • スプレッドシートから従業員と日付の情報を取得し、ChatGPTにリクエストを送信し、その応答をシフト表に反映します。
従業員と日付の取得
  // A6以降の従業員名を取得
  const members = sheet.getRange("A6:A" + sheet.getLastRow()).getValues()
    .flat()
    .filter(name => name);

  // B4:AF4の範囲から日付リストを取得
  const dates = sheet.getRange("B4:AF4").getValues()[0].filter(date => date);
  • 従業員リストはシートのA列6行目以降から取得します。
  • 日付リストはB4の範囲から取得します。
プロンプト生成
  // プロンプトを生成
  const prompt = createShiftPrompt(members, dates);
  • createShiftPrompt関数でChatGPTに送信するプロンプトを作成します。
  • プロンプトには、従業員リストと日付リスト、シフトの割り当てルールを指定しています。
ChatGPT APIリクエスト
  // ChatGPT APIにリクエストを送信
  const chatGPTResponse = callChatGPTAPI(prompt);
  • callChatGPTAPI関数を使って、生成したプロンプトをChatGPTに送信します。
  • この関数ではAPIリクエストを行い、JSON形式で応答を取得し、シフト表を生成します。
シフトの割り当て
  // ChatGPTからの応答をシフト表に反映
  if (chatGPTResponse) {
    applyShiftAssignments(sheet, chatGPTResponse, 2, 6); // 日付の開始列(B列=2)と従業員の開始行(6行目)
  }
}
  • applyShiftAssignments関数を使って、ChatGPTの応答を元にシフト表をスプレッドシートに反映させます。
callChatGPTAPI関数
function callChatGPTAPI(prompt) {
  const url = 'https://api.openai.com/v1/chat/completions';
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': `Bearer ${OPENAI_API_KEY}`
    },
    payload: JSON.stringify({
      model: 'gpt-4o',
      messages: [
        { role: 'system', content: 'You are an AI that only outputs valid JSON based on user prompts.' },
        { role: 'user', content: prompt }
      ],
      max_tokens: 4500,
      temperature: 0.7
    })
  };
  • APIエンドポイントhttps://api.openai.com/v1/chat/completionsにリクエストを送信します。
  • method: 'post': POSTリクエストを使用。
  • contentType: 'application/json': JSON形式のリクエスト。
  • headers: AuthorizationヘッダーにAPIキーを含めます。
  • payload: モデルとして’gpt-4o’を使用し、プロンプトと一緒に送信します。
レスポンス処理
  try {
    const response = UrlFetchApp.fetch(url, options);
    const json = JSON.parse(response.getContentText());

    // ChatGPTの応答を取得
    let content = json.choices[0].message.content.trim();

    // JSON形式の部分のみを抽出する処理
    const jsonStart = content.indexOf('{');
    const jsonEnd = content.lastIndexOf('}') + 1;
    if (jsonStart !== -1 && jsonEnd !== -1) {
      content = content.slice(jsonStart, jsonEnd);
    }

    // JSONパースの試行
    try {
      return JSON.parse(content); // JSONとしてパースを試みる
    } catch (e) {
      Logger.log("Error: The response is not valid JSON.");
      Logger.log("Received content: " + content);
      Logger.log("Prompt used: " + prompt);
      return null; // JSONとしてパースできない場合
    }
  } catch (e) {
    Logger.log("Error: " + e.toString());
    return null;
  }
}
  • ChatGPTの応答を受け取り、その中から有効なJSON部分のみを抽出してパースします。
  • JSONの開始と終了位置を特定し、不要な部分を除去して有効なJSONを抽出する処理を行います。
  • JSONとして正しくパースできない場合、ログにエラーメッセージを記録します。
applyShiftAssignments関数
function applyShiftAssignments(sheet, shiftAssignments, startDateColumn, startRow) {
  // ChatGPTの応答をシフト表に反映
  Object.keys(shiftAssignments).forEach((date, dateIndex) => {
    const col = startDateColumn + dateIndex;
    const assignments = shiftAssignments[date];
    let rowIndex = startRow;

    // 各従業員のシフトを対応するセルに配置
    Object.keys(assignments).forEach((member) => {
      const shift = assignments[member];
      sheet.getRange(rowIndex, col).setValue(shift);
      rowIndex++;
    });
  });
}
  • この関数は、ChatGPTの応答に基づいてシフト表をスプレッドシートに反映します。
  • Object.keys(shiftAssignments).forEach((date, dateIndex) => {...}): 各日付についてシフトをスプレッドシートに配置します。
  • 各従業員のシフトをスプレッドシートに割り当てるために、対応する行と列にシフト情報を入力します。
STEP
スクリプトを保存

スクリプトを書いたら、名前をつけて保存します。

(例:「シフト表作成」)

STEP
スクリプトを実行

スクリプトを実行して、ChatGPTにシフト表を作成してもらいます。

初めてそのスクリプトを実行する場合は権限の確認が必要です。

そのため、『権限を確認』を押します。

許可の詳細手順

「詳細」をクリックします。

無題のプロジェクト(安全ではないページ)に移動」をクリックします。

許可」をクリックします。

STEP
スプレッドシートにシフト割り当てが反映

従業員と日付ごとのシフトが自動的に割り当てられます。

(シフトの色分けは条件付き書式で設定)

ChatGPTに指示した内容

  • 以下の従業員リストと日付に基づいて、シフト表を厳密にJSON形式でのみ出力してください。
  • 以下の指示を厳密に守り、余分な説明を含めないでください。

    条件:
    • 各日には必ず「早番」が3名、「遅番」が3名、それ以外の従業員は「休み」として割り当ててください。
    • シフトは以下の3種類です:「早」(早番)、「遅」(遅番)、「休」(休み)。
    • 各従業員は、できるだけ公平に「早」「遅」「休」のシフトが割り当てられるようにしてください。
    • 同じ従業員が連続して「早」または「遅」シフトに入らないようにしてください。
    • 全員のシフトが均等になるようにし、できるだけランダムに割り当ててください。
    • 各日付のシフトには、毎回異なる従業員が含まれるようにしてください。
    • 各従業員が各シフト(「早」、「遅」、「休」)をできるだけ均等に受けるようにしてください。

  • 出力は必ず有効なJSON形式で次のようにしてください:
  • { “10/1”: {“従業員名1”: “早”, “従業員名2”: “早”, “従業員名3”: “早”, “従業員名4”: “遅”, “従業員名5”: “遅”, “従業員名6”: “遅”, “従業員名7”: “休”, “従業員名8”: “休”}, “10/2”: {“従業員名1”: “遅”, “従業員名2”: “早”, “従業員名3”: “早”, “従業員名4”: “早”, “従業員名5”: “遅”, “従業員名6”: “遅”, “従業員名7”: “休”, “従業員名8”: “休”} }` + ` 従業員リスト: ${members.join(“, “)} ` + `日付リスト: ${dates.join(“,”)}

今回ChatGPTに指示した内容は上記の通りです。

指示条件を変更することで、希望に合った内容のシフト割り当てを作成できます。

さらに効率的なシフト作成のためのツール導入

簡易的なシフト表であればスプレッドシートで作成したもので十分活用できますが、さらに効率的に行うには専用のシフト管理ツールを導入するのも有効です。

これらのツールを利用することで、より高度なスケジュール調整や従業員の希望を考慮したシフト割り当てが可能になります。

また、ツールによってはIT導入補助金の対象となり、費用面での負担を軽減することができます。

どのITツールが補助金の対象となるかは、IT導入補助金事務局の「ITツール検索」で確認できます。

>>ITツール検索

(ツール例:勤務シフト作成お助けマンなど)

シフト作成業務の効率化をさらに進めたい方は、こうしたツールの導入を検討してみてはいかがでしょうか?

まとめ

ChatGPTとスプレッドシートを組み合わせることで、シフト表の作成が効率的になります。

条件をさらに細かく指示すれば、その条件に応じた結果をスプレッドシートに反映してくれます。

AIツールを活用することで、手作業でシフトを割り当てる手間やミスを省き、業務の質の向上が期待できます。

弊社では、Google Apps Script(GAS)を活用した業務効率化のサポートを提供しております。

GASのカスタマイズやエラー対応にお困りの際は、ぜひお気軽にご相談ください。

また、ITツールの導入支援やIT導入補助金の申請サポートも行っております。

貴方の業務改善を全力でサポートいたします。

お問い合わせはこちら

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

コメント

コメントする

目次