本記事では、以下のデモのように、GPTを自分のGoogle カレンダーと連携して、

  1. 予定の確認

  2. 予定の削除

  3. 予定の編集

  4. 予定の追加

を行う方法を、ステップバイステップで解説します。

重要な点として、GAS + GPTs のシステム構成だとサーバー代、GPT代ともにゼロコストで運用できる点があります。

GAS版の秘書GPTは、適宜、更新予定です。
GPTsラボへのご参加がおすすめです。


作り方

早速ですが、GASでカレンダー連携するGPTの作り方をみていきます。

Step 1. Google Apps Script を作成する

以下のサイトにアクセスして、新しいプロジェクトをクリックします。

https://script.google.com/home

重要な点として、このGoogle Apps Scriptを作成したGoogleアカウントに紐づくカレンダーを操作することになります。

まず、元々書いてあるコードを全て削除したのち、以下のコードをすべて貼り付けます。

function doPost(e) {
  try {
    if (!e.postData) {
      throw new Error("No post data received");
    }
    const requestBody = JSON.parse(e.postData.contents);
    let result;

    // Determine the type of request based on the operation field
    switch (requestBody.operation) {
      case "createEvent":
        result = setCalendarEvent_(requestBody);
        break;
      case "viewEvents":
        result = viewCalendarEvents_(requestBody);
        break;
      case "deleteEvent":
        result = deleteCalendarEvent_(requestBody);
        break;
      case "updateEvent":
        result = updateCalendarEvent_(requestBody);
        break;
      default:
        throw new Error("Invalid operation specified");
    }

    return ContentService.createTextOutput(result)
      .setMimeType(ContentService.MimeType.JSON);
  } catch (error) {
    return ContentService.createTextOutput(
      JSON.stringify({ "error": error.toString() })
    ).setMimeType(ContentService.MimeType.JSON);
  }
}


function validateRequiredArgs(args, requiredArgs) {
  const missingArgs = requiredArgs.filter(arg => !args[arg]);
  if (missingArgs.length > 0) {
    return `Missing required argument(s): ${missingArgs.join(", ")}`;
  }
  return null; // No missing arguments
}


function viewCalendarEvents_(args) {
  try {
    if (validateRequiredArgs(args, ['startDate', 'endDate'])) {
      return JSON.stringify({ "error": validationError });
    }

    const { startDate, endDate } = args;
    const events = CalendarApp.getDefaultCalendar().getEvents(new Date(startDate), new Date(endDate));
    const eventDetails = events.map(event => {
      const attendees = event.getGuestList().map(guest => guest.getEmail()).join(", ");
      return {
        title: event.getTitle(),
        start: formatDate_(event.getStartTime()),
        end: formatDate_(event.getEndTime()),
        location: event.getLocation(),
        description: event.getDescription(),
        attendees: attendees,
        id: event.getId()
      };
    });
    return JSON.stringify(eventDetails);
  } catch (e) {
    return JSON.stringify({ "error": `Failed to get calendar events with the Error: ${e}` });
  }
}


function deleteCalendarEvent_(args) {
  try {
    if (validateRequiredArgs(args, ['eventId'])) {
      return JSON.stringify({ "error": validationError });
    }
    const { eventId } = args
    const event = CalendarApp.getDefaultCalendar().getEventById(eventId)
    if (!event) {
      throw Error(`Could not found Event:${eventId}`)
    }
    event.deleteEvent()
    return `The Event(ID: ${eventId}) removal was successful.`
  } catch (e) {
    return `Failure to delete. Error: ${e}`
  }
}

function updateCalendarEvent_(args) {
  try {
    if (validateRequiredArgs(args, ['eventId'])) {
      return JSON.stringify({ "error": validationError });
    }

    let { eventId, title, startDate, endDate, description, location, attendeesToBeAdded, attendeesToBeDeleted } = args
    const event = CalendarApp.getDefaultCalendar().getEventById(eventId)
    if (!event) {
      throw Error(`Could not found Event:${eventId}`)
    }

    if (title) event.setTitle(title)
    else {
      title = event.getTitle()
    }
    if (startDate && endDate) event.setTime(new Date(startDate), new Date(endDate))
    else {
      startDate = event.getStartTime()
      endDate = event.getEndTime()
    }
    if (description) event.setDescription(description)
    else {
      description = event.getDescription()
    }
    if (location) event.setLocation(location)
    else {
      location = event.getLocation()
    }
    if (attendeesToBeAdded) {
      attendeesToBeAdded.forEach(guest => event.addGuest(guest))
    }
    if (attendeesToBeDeleted) {
      attendeesToBeDeleted.forEach(guest => event.removeGuest(guest))
    }
    const attendees = event.getGuestList().map(guest => guest.getEmail())
    const startDateFormatted = formatDate_(new Date(startDate));
    const endDateFormatted = formatDate_(new Date(endDate));
    const attendeesString = attendees.join('、');

    return `The event update was successful. Updated event titled ${title} from ${startDateFormatted} to ${endDateFormatted} at location ${location} with attendees ${attendeesString}`
  } catch (e) {
    return `Failure to update event. Error: ${e}`
  }
}

function setCalendarEvent_(args) {
  try {
    if (validateRequiredArgs(args, ['title', 'startDate', 'endDate'])) {
      return JSON.stringify({ "error": validationError });
    }

    const { title, startDate, endDate, description, location, attendees = [] } = args;
    const calendar = CalendarApp.getDefaultCalendar();

    const event = calendar.createEvent(
      title,
      new Date(startDate),
      new Date(endDate),
      {
        description: description,
        location: location ?? '',
        guests: attendees.join(','),
        sendInvites: true
      }
    );

    const startDateFormatted = formatDate_(new Date(startDate));
    const endDateFormatted = formatDate_(new Date(endDate));
    const attendeesString = attendees.join('、');

    return `Successfully created the event titled ${title} from ${startDateFormatted} to ${endDateFormatted} at location ${location} with attendees ${attendeesString}. The Event ID is: ${event.getId()}`;
  } catch (e) {
    return `Failed to add event to Calendar with the Error message: ${e}.`;
  }
}


function formatDate_(date) {
  const year = date.getFullYear();
  const month = ("0" + (date.getMonth() + 1)).slice(-2);
  const day = ("0" + date.getDate()).slice(-2);
  const hour = ("0" + date.getHours()).slice(-2);
  const minute = ("0" + date.getMinutes()).slice(-2);
  return `${year}年${month}月${day}日 ${hour}:${minute}`;
}

Step 2. Google Apps Script をデプロイする

続いて、このスクリプトをデプロイします。

  • 種類の選択」で「ウェブアプリ」 を選択後、

  • 次のユーザーとして実行」を「自分」に、

  • アクセスできるユーザー」 を 「全員」 に設定します

  • 右下の「デプロイ」 をクリックします

  • 次の画面で「アクセスを承認」を押し、Googleアカウントへログインします。

  • 備考)エンタープライズアカウント以外では、次の画面で以下のように、「このアプリはGoogleで確認されていません」という画面が出る場合があります。


このメッセージが表示されたら、「詳細」をクリックし、「【プロジェクト名】(安全でないページ)」をクリックして進みます

カレンダーへのアクセスの許可が求められるので、許可をクリックします。

デプロイが完了すると、URL が発行されるため、この URL をコピーしておきます。

Step 3. GPTの設定を行なう

GPT Builder を使ったことがない場合には以下の記事をまずご覧ください:

https://agi-labo.com/articles/nb7ca3c406730

名前、画像、プロンプトなどを好きに設定します。

勤務時間帯などの、自社の情報などをプロンプトに入れ込むことでより便利なGPTになっていきます。

画像生成、ブラウジング、Code Interpreter などの Capabilities についても、お好きな設定にしましょう。

Step 4. Actions の設定を行なう

続いて、Actions を設定していきます。
まずは、[Create new action]をクリックします。以下の画面になります:

以下のYAMLファイルの「ここにデプロイしたGASのIDを挿入します」と書いてある部分を、先ほどデプロイしたGASのIDに変更したのち、Schema の欄にそのままこれをコピペします:

openapi: "3.1.0"
info:
  title: "Calendar Management"
  description: "API for creating, viewing, editing and deleting events in Google Calendar using a single endpoint"
  version: "v1.0.0"
servers:
  - url: "https://script.google.com"
paths:
  /macros/s/ここにデプロイしたGASのIDを挿入します/exec:
    post:
      description: "Endpoint for creating, viewing, and deleting calendar events"
      operationId: "CalendarOperations"
      requestBody:
        description: "Details for either creating, viewing, or deleting calendar events"
        required: true
        content:
          application/json:
            schema:
              type: "object"
              required: ["operation"]
              properties:
                operation:
                  type: "string"
                  enum: ["createEvent", "viewEvents", "deleteEvent", "updateEvent"]
                  description: "Specify the operation type"
                title:
                  type: "string"
                  description: "The title of the event (required for createEvent)"
                startDate:
                  type: "string"
                  format: "date-time"
                  description: "The start date and time of the event (required for createEvent and viewEvents)"
                endDate:
                  type: "string"
                  format: "date-time"
                  description: "The end date and time of the event (required for createEvent and viewEvents)"
                attendees:
                  type: "array"
                  description: "List of attendees' email addresses (required for createEvent)"
                  items:
                    type: "string"
                eventId:
                  type: "string"
                  description: "The ID of the event to be deleted (required for deleteEvent)"
                location:
                  type: "string"
                  description: "New location for the event (optional)"
                attendeesToBeAdded:
                  type: "array"
                  items:
                    type: "string"
                  description: "List of email addresses to be added to the event (optional)"
                attendeesToBeDeleted:
                  type: "array"
                  items:
                    type: "string"
                  description: "List of email addresses to be removed from the event (optional)"
      responses:
        '200':
          description: "Successful operation response"
          content:
            application/json:
              schema:
                oneOf:
                  - type: "string"
                  - type: "array"
                    items:
                      $ref: "#/components/schemas/EventInfo"
components:
  schemas:
    EventInfo:
      type: "object"
      properties:
        title:
          type: "string"
          description: "The title of the event"
        start:
          type: "string"
          description: "The start time of the event"
        end:
          type: "string"
          description: "The end time of the event"
        location:
          type: "string"
          description: "The location of the event"
        description:
          type: "string"
          description: "The description of the event"
        attendees:
          type: "string"
          description: "List of attendees' email addresses"
        id:
          type: "string"
          description: "The unique ID of the event"

以下の画像のようになっていればOKです:

はい、実はこれでもう、秘書GPTの完成です🙌

実際に使えるか、Preview画面で確かめてみましょう!

カレンダーを見て、実際に予定が入っていることを確かめます:

あとは、保存です。

注意点として、このGPTは必ず、Only me でのみ利用するように注意してください。他の人に使ってもらうものではありません。(※そもそも Privacy Policy URL を入力していないためURLを共有することは物理的にできません)

あとは、使い倒すだけです!
いくら使っても ChatGPT Plus 代金しかかかりません。

https://twitter.com/ctgptlb/status/1731823447414637031?s=20

このGAS版秘書GPTについては、メール関連操作、会食検索機能など、今後もアップデートして機能拡充していく予定です。

こんな機能があったら便利などもコメントあればお寄せください!