見出し画像

GoogleカレンダーとGPTの接続を1回でまとめてやろうとした話(GPTs制作)


はじめに

こんにちは!
KIRIKO.tech(株)代表の佐藤です。

今日のお話はタイトル通り
「GoogleカレンダーとGPTの接続を1回でやろうとした話」
になります。

結論からいうと出来ました!出来ましたが、新たな問題が・・・。

きっかけ

お世話になっているChatGPT研究所さんの記事を参考にGoogleカレンダーと連携できるGPTsを作成しました。

それがこの記事にもある受験生の勉強スケジュールを教えてくれるスーパーチューターGPTsというものです。

その名の通り、勉強スケジュールを立ててくれた上でその予定をGoogleカレンダーに落とし込んでくれるすごいやつです。

ただ、ぼくの記事で触れていますが問題点があります。
それは、予定の数だけGPTsを回してしまうとういう点です。


5教科の予定分GPTが回っています。

この意味について説明すると、GPTsはChat-GPTにおいて有料対象になります。
そして、接続つまりは使用制限が設けられているのです。

当初は40回くらいでしたが、どんどん使用制限が少なくなっている気が…。

なので1週間分の予定なんて作った日にはその日のGPTsとの戯れは終了になるわけです。

これはやばい、なのでどうにかして一括で処理できないものかと考えました。

また、AItechさんがGPTsのハッカソンを開催していたので、これは開発するいい機会だ!!なんて思えたのもあります。
実際、期日期限がある中での開発は気づきや閃きを与えてくれることも多く、積極的に参加したいと改めて今回感じました。


受験生のスケジュールを考えてくれるスーパーチューターの仕組み

彼の仕組みは、GoogleカレンダーとOAuth接続にあります。

GoogleログインのOAuth設定

OAuthを使用してGoogleアカウントでのログインを設定することで、ユーザーは自分のGoogleアカウントを安全に利用してログインできるようになります。このプロセスは、ユーザーのデータ保護を強化したり、アプリケーションの信頼性を向上させたりしてます。

GoogleカレンダーとGPTの連携

GPTをGoogleカレンダーと連携させることにより、カレンダーのイベントを管理したり、スケジュールを問い合わせたりするなどの操作が可能になります。これにより、カレンダーの利用がより便利かつ効率的になります。

GASとの違いと併用

本設定ではコーディングが不要で、GASと異なりますが、GASとの併用も可能です。GASとの主な違いは、コーディングの必要性と、Googleアカウントでログインできる点です。さらに、不特定多数への公開にはGCPからの認可が必要ですが、限定的な共有は容易に実現できます。

その他

また、OpenAPIの仕様を利用することで、Googleカレンダー以外のGoogleアプリケーションとも容易に連携できるメリットがあります。これにより、様々なアプリケーションを組み合わせて使用することが可能になり、ユーザーのニーズに応じたカスタマイズが行えます。

接続の詳細は参考にさせていただいたChat-GPT研究所さんの有料記事に詳しく書かれています。

これが大まかな仕組みとなるのですが、やはり一括で処理するにはGAS(Google App Sheet)やPythonを使ったループ処理だろうと感じました。


ちなみにこの時のツイートです、皆さんフォローしてねw

GASと連携させてみる

今回の流れを改めて整理しましょう。

作成背景

Google カレンダーとGPTを連動させたGPTsを作成した時、会話の中で注文を入れた予定の数の分、GPTを回転させていることがわかった。

GPTsの使用回数制限が少なっていると感じるこの頃だったので、GASのループ処理でGPTが一括処理できるようGoogle App Sheet actionsを整理

GPTsのactionsを組み立てる。

GASのコード

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 "createEvents": // 複数のイベントを作成するための操作を追加
        result = setCalendarEvents_(requestBody.events); // 複数のイベントを処理するため、events配列を渡す
        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) {
    throw new Error(`Missing required argument(s): ${missingArgs.join(", ")}`);
  }
}

function viewCalendarEvents_(args) {
  validateRequiredArgs(args, ['startDate', 'endDate']);
  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);
}

function deleteCalendarEvent_(args) {
  validateRequiredArgs(args, ['eventId']);
  const { eventId } = args;
  const event = CalendarApp.getDefaultCalendar().getEventById(eventId);
  if (!event) {
    throw new Error(`Could not find Event:${eventId}`);
  }
  event.deleteEvent();
  return JSON.stringify({ "message": `The Event(ID: ${eventId}) removal was successful.` });
}

function updateCalendarEvent_(args) {
  validateRequiredArgs(args, ['eventId']);
  let { eventId, title, startDate, endDate, description, location, attendeesToBeAdded, attendeesToBeDeleted } = args;
  const event = CalendarApp.getDefaultCalendar().getEventById(eventId);
  if (!event) {
    throw new Error(`Could not find Event:${eventId}`);
  }

  if (title) event.setTitle(title);
  if (startDate && endDate) event.setTime(new Date(startDate), new Date(endDate));
  if (description) event.setDescription(description);
  if (location) event.setLocation(location);
  if (attendeesToBeAdded) attendeesToBeAdded.forEach(guest => event.addGuest(guest));
  if (attendeesToBeDeleted) attendeesToBeDeleted.forEach(guest => event.removeGuest(guest));

  return JSON.stringify({ "message": `The event update was successful. Event ID: ${eventId}` });
}

function setCalendarEvents_(eventsData) {
  const results = []; // 結果を格納する配列

  eventsData.forEach((eventData) => {
    try {
      validateRequiredArgs(eventData, ['title', 'startDate', 'endDate']);
      const { title, startDate, endDate, description, location, attendees = [] } = eventData;
      const calendar = CalendarApp.getDefaultCalendar();

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

      // LINEにイベント概要を送信
      broadcastLineMessage(createEventMessage(title, startDate, endDate, description, location, attendees));

      results.push({
        "message": `Successfully created the event titled ${title}.`,
        "eventId": event.getId()
      });
    } catch (e) {
      results.push({ "error": `Failed to add event: ${e.message}` });
    }
  });

  return JSON.stringify(results);
}

// LINE Messaging APIによるメッセージ送信の関数
// LINE Messaging APIのアクセストークン
const LINE_CHANNEL_ACCESS_TOKEN = 'ここに自分のアクセストークンを入れてください';

/**
 * メッセージを全ユーザーにブロードキャストします。
 * @param {string} messageText ブロードキャストするメッセージのテキスト
 */
function broadcastLineMessage(messageText) {
  const payload = {
    "messages": [
      {
        "type": "text",
        "text": messageText
      }
    ]
  };

  const options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${LINE_CHANNEL_ACCESS_TOKEN}`
    },
    "payload": JSON.stringify(payload)
  };

  const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/broadcast", options);
  const result = JSON.parse(response.getContentText());

  if (response.getResponseCode() === 200) {
    Logger.log(`Message broadcasted successfully: ${result.result}`);
  } else {
    Logger.log(`Failed to broadcast message: ${result.error}`);
  }
}


// イベントの概要を作成する関数
function createEventMessage(title, startDate, endDate, description, location, attendees) {
  return `Event Created: ${title}\nStart: ${formatDate_(new Date(startDate))}\nEnd: ${formatDate_(new Date(endDate))}\nDescription: ${description}\nLocation: ${location}\nAttendees: ${attendees.join(', ')}`;
}

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}`;
}

GASのコードの方はこうになりました。
これを「実行ユーザー=自分,アクセスユーザー=誰でも」に設定しウェブアプリとしてデプロイ
デプロイできたらデプロイIDをメモしておきます。

const LINE_CHANNEL_ACCESS_TOKEN = 'ここに自分のアクセストークンを入れてください';

ここは自分のアクセストークンを入れてください

GPTsのactions

openapi: "3.1.0"
info:
  title: "Calendar Management"
  description: "API for creating, viewing, editing, and deleting events in Google Calendar using a single endpoint. Supports bulk event creation."
  version: "v1.0.0"
servers:
  - url: "https://script.google.com"
paths:
  /macros/s/{ここは自分のGASのデプロイID}/exec:
    post:
      description: "Endpoint for creating, viewing, editing, and deleting calendar events. Supports bulk event creation."
      operationId: "CalendarOperations"
      requestBody:
        description: "Details for creating, viewing, editing, or deleting calendar events. For bulk creation, provide an array of event objects within the 'events' field."
        required: true
        content:
          application/json:
            schema:
              type: "object"
              required: ["operation"]
              properties:
                operation:
                  type: "string"
                  enum: ["createEvents", "viewEvents", "deleteEvent", "updateEvent"]
                  description: "Specify the operation type"
                events:
                  type: "array"
                  description: "Array of event objects for bulk creation (used only with 'createEvents' operation)"
                  items:
                    $ref: "#/components/schemas/Event"
                # Other properties for single event operations like createEvent, viewEvents, deleteEvent, updateEvent
                # e.g., title, startDate, endDate, attendees, eventId, etc.
      responses:
        '200':
          description: "Successful operation response"
          content:
            application/json:
              schema:
                oneOf:
                  - type: "string"
                  - type: "array"
                    items:
                      $ref: "#/components/schemas/EventInfo"
components:
  schemas:
    Event:
      type: "object"
      required: ["title", "startDate", "endDate"]
      properties:
        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 (optional for createEvent)"
          items:
            type: "string"
        # Additional properties like description, location, etc.
    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"

これがGPTsのactionsになります。

paths:
  /macros/s/{ここは自分のGASのデプロイID}/exec:

ここは自分のデプロイID

こんな感じで骨を組みました。

成果物

そしてやってみると・・・


おっ!???これは!??

複数の予定に関して、紫は一回。
これは、もしや・・・・・!??????

出来てる!!!

やったぁぁぁぁぁぁ!!!出来てます!!!!


これが、僕の作成した「もう一人の僕」という予定管理GPTsになります。


なぜスクショ??

そう、だったらこのGPTs共有したいですよね、ところがとっこい彼はなんと!!

僕のアカウントしか結びつかないんです・・・
なのでこのGPTsに送った情報は全て僕のGoogleカレンダーに共有されます。
僕の予定とんでもないことになりますよ、まじで

ただ、僕自身が現在、起業したばかりでてんやわんやな状態であるため、「今自分が欲しいGPTsを考えよう」という点では、自分のタスク管理、整理をやってくれるもう一人の僕GPTsとてもナイスなんです。

なんならこれ使ってます!!笑

LINEとの接続もこのようにしれっとやってます!
需要があれば、またこの辺りの解説記事も書ければと思います。


もちろん文言含め、カスタマイズ可能


注意点と課題

自分自身、つまりはKIRIKO専用のGPTsになっています。
そもそも今回の開発物作成のきっかけは、GPTsとカレンダーAPIを直接結びつけた時にGPTが複数回ってしまうことへの解決にありました。
現状カレンダーと直接OAuthで接続すれば、公開GPTで全員が使うこと(制限あり)が出来るが何度も言うようにその予定の都度GPTを回すことなり、使用制限がすぐにきてしまうのです。

今回の「もう一人の僕」仕様だとその問題は解決できる代わりに、誰が使ってもGAS実行アカウントがKIRIKOのアカウントとしてのアクセスになってしまうので使用例としては、自分自身で使うことが大前提となります。
(多くの人へ使って貰うにはその人へ向けてご自身のGPTを開発しますよ!とかのアプローチが必要になるのかなぁ)

※GASを使ってもアクセスユーザーのカレンダーに書き込めるように出来る方法をもし知っていましたら教えていただけると幸いです。

まとめ

いかがだったでしょうか、以上が、GoogleカレンダーとGPTの接続を1回でやろうとした話(GPTs制作)になります。

今後は課題の解決、及びこのGPTsの改良をしていく予定です。

こんな感じで生成AI、Chat‐GPTおもしろいので是非まださわっていない方いらしたらさわってみてください~~~

初心者向けにYouTube動画も撮影しているので良かったらチャンネル登録お願いします

それでは皆様、よい生成AIライフを!!