Link and Motivation Developers' Blog

リンクアンドモチベーションの開発者ブログです

Google Workspace Addon を作成し、社内アプリとして展開する方法

リンクアンドモチベーションのデータユニット・RevOpsチームの みく / Ryo Koizumi (@slpwalks) です。

リンクアンドモチベーションのRevOpsチームでは、単なる社内データ分析やデータの入出力の管理を行うだけでなく、現場コンサルタントチームのカスタマーサクセスやアポイントメントの行動改善までサポートする、かなり幅の広い活動を行なっています。

現在データユニットではポジションを絶賛募集中なので、ご興味ある方は一度カジュアル面談でお話ししましょう!

概要

  • Google Workspace には、アドオンという形でホストアプリの機能を拡張するアプリ(Google Workspace Addon)をインストールすることが出来ます。
  • アドオンは Google Apps Script の仕組みで作成することができ、さらに社内ドメイン向けに公開し、社内の方々にインストールしてもらう形で使ってもらうことが出来ます。
  • スプレッドシートを拡張してアプリを作成する方法に比べて、組織内の大量のユーザーに同時にリリース出来る点など多くのメリットがあります。

Google Workspace Addon とは?

  • Google Workspace アプリ(ドキュメント・スプレッドシートGmail・カレンダーなど)を拡張する仕組みです。
  • 各アプリの右のサイドバーから起動します。

社内では Google Workspace Addon をどのように利用しているか?

社内では Google Workspace Addon の仕組みを用いた以下のようなツールが展開されています。

  • アポイントメントログ収集ツール(アポログ簡単入力くん)
    • RevOps チームでは、現場コンサルタント向けに Google カレンダーを拡張したアプリを提供し、アポイントメントログをより効率的に収集するツールを運用しています。
    • 収集したアポイントメントログはヘルススコアなどに利用しています。Google Workspace Addon の仕組みを提供することで、アポイントメントログの入力工数が1/5ほどになり、入力率も3倍に改善することが出来ました。
    • 現在、LLMと組み合わせ、アポイントメントログの収集だけではなく個人の行動改善を行えるようにする機能(ヘルススコアAIアドバイザー)を開発中です。
  • また、RevOps 以外のチームでも同様の仕組みでアプリケーションを提供し、社内のさまざまな業務を効率化しています。

どのようなことが実現できる?

  • ホストアプリを開いた時に、特定のUIを表示することが出来ます。(ホームページ)
  • Gmailを開いた時や、メッセージを作成する時、カレンダーを開いた時など特定のイベントに反応し、イベントに応じたUIを表示することが出来ます。(コンテキストインタフェース)
  • ボタンやドロップダウンメニューを作成し、特定の関数を実行するアクションを設定することが出来ます。

vs. エディタアドオン

  • Google Workspace アプリを拡張する仕組みとしては、下記の2種類の仕組みが用意されています。
    • Google Workspace Addon (今回の仕組み)
    • エディタアドオン(レガシーな仕組み)
  • 2つの主な違いは以下になります。
    • Google Workspace Addon は一つのデプロイにつき複数のアプリを拡張出来ますが、エディタアドオンはドキュメント用・スプレッドシート用などそれぞれの単一のアプリを拡張する仕組みになります。
    • Google Workspace Addon は Card Service という JSON ベースで UI を構築する仕組みが利用できます。エディタアドオンは通常のHTMLとCSSを利用してUIを構築します。
  • より詳しい違いは公式ドキュメントを参照してください。基本的にはより新しい Google Workspace Addon の仕組みの方が楽にアドオンを拡張できるようになっています。

社内ツールとしてのスプレッドシートと比べてのメリット・デメリット

社内ツールとして、Google Apps Script を用いて拡張したスプレッドシートを社内展開するようなユースケースが多いと思います。そのような場合と比べての Google Workspace Addon のメリット・デメリットをまとめます。

メリット

  • 普通のWebアプリっぽいUIを簡単に作成することが出来る
  • ログインユーザーの情報を取得し、UIを出し分けることができる。
  • 単一のデプロイで多くの人数に対して利用させることができ、さらに複数のアプリにまたがってデプロイできる。
    • スプレッドシートだと安全に同時編集できる人数が限られるため、一つのシートを大規模に展開することはできず、スクリプトやUIの更新をそれぞれのシートに分割して行う必要があります。
    • Google Workspace Addon の場合、ユーザーに一度インストールを行なってもらえば、以降は一つの環境にデプロイを行うことで全てのユーザーに向けて一斉に更新することができます。

デメリット

実装のハンズオン

  • ここからは、 Google Workspace Addon の仕組みを使い、サイドバーから OpenAI API 経由で GPT に問い合わせを行えるアプリを作成します。

UI の作成

  • UI については Card Builder で作成できます。
  • 基本的には Card という全体の中に複数の Section が存在し、Section の中に複数の Widget(ボタンやインプットフォームなど)が存在します。
  • UIを作成すると、右に JSONJSON を出力するための Apps Script のコードを出力してくれます。後々に作成するスクリプトにコードをコピーすることでUIを構成できるようになります。

Card Builder を用いた UI の作成

アプリの作成

GAS - 新しいプロジェクトの作成

  • 先に設定で「appsscript.json マニフェストファイルをエディタで表示する」にチェックを入れ、マニフェストファイルを表示できるようにしておきます。

マニフェストファイルを表示する設定

マニフェストファイルの編集

  • エディタに戻り、先ほど表示した appsscript.json を表示してください。
  • 以下のようなマニフェストを設定します。
{
  "timeZone": "Asia/Tokyo",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "addOns": {
    "common": {
      "homepageTrigger": {
        "runFunction": "onDefaultHomePageOpen",
        "enabled": true
      },
      "layoutProperties": {
        "primaryColor": "#673ab7",
        "secondaryColor": "#9c27b0"
      },
      "logoUrl": "https://ssl.gstatic.com/docs/script/images/logo/script-64.png",
      "name": "GPT問い合わせくん",
      "universalActions": [
        {
          "label": "Open Help URL",
          "openLink": "https://www.example.com/help"
        }
      ],
      "useLocaleFromApp": true
    },
    "calendar": {},
    "drive": {},
    "gmail": {},
    "docs": {},
    "sheets": {},
    "slides": {}
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/calendar.addons.execute",
    "https://www.googleapis.com/auth/drive.addons.metadata.readonly",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.locale",
    "https://www.googleapis.com/auth/script.scriptapp",
    "https://www.googleapis.com/auth/drive.file",
    "https://www.googleapis.com/auth/documents.currentonly",
    "https://www.googleapis.com/auth/spreadsheets.currentonly",
    "https://www.googleapis.com/auth/presentations.currentonly"
  ],
  "urlFetchWhitelist": [
    "https://api.openai.com/"
  ]
}
  • addOns 以下がアドオンのマニフェストになります。
    • addons.common に名前やロゴのURL(必須)を設定します。
    • homepageTrigger.runFunction にアプリを開いた時に実行する関数を指定します。
    • universalActions はアプリメニューを開いた時に実行する関数を指定できます。
    • calendar 以下は各アプリの固有の設定を書くことができます。キーを追加することでそのアプリでアドオンが有効になります。今回は共通のホームページを利用し、アプリ固有のイベントには反応しないため中身は書いていません。
  • oauthScopes にはそのアプリが利用するスコープを記述します。
  • urlFetchWhiteListにはアプリが外部接続するURLのドメインリストを記述しておきます。

UI の表示とテスト

  • 先ほどのマニフェストでホームページに onDefaultHomePageOpen という関数を指定したので、その関数を作成します。
  • 先ほどの UI の作成で buildCard という関数が作成されたので、そのまま return します。
function buildCard() {
    let cardSection1TextInput1 = CardService.newTextInput()
        .setFieldName('message')
        .setTitle('GPTに聞く内容')
        .setMultiline(true);

    let cardSection1ButtonList1Button1Action1 = CardService.newAction()
        .setFunctionName('send')
        .setParameters({});

    let cardSection1ButtonList1Button1 = CardService.newTextButton()
        .setText('送信する')
        .setTextButtonStyle(CardService.TextButtonStyle.TEXT)
        .setOnClickAction(cardSection1ButtonList1Button1Action1);

    let cardSection1ButtonList1 = CardService.newButtonSet()
        .addButton(cardSection1ButtonList1Button1);

    let cardSection1DecoratedText1 = CardService.newDecoratedText()
        .setTopLabel('GPTからの返答')
        .setText('hogehoge');

    let cardSection1 = CardService.newCardSection()
        .addWidget(cardSection1TextInput1)
        .addWidget(cardSection1ButtonList1)
        .addWidget(cardSection1DecoratedText1);

    let card = CardService.newCardBuilder()
        .addSection(cardSection1)
        .build();
    return card;
}

function onDefaultHomePageOpen() {
  return buildCard()
}
  • テストは右上のデプロイをクリックし、「デプロイをテスト」を選択します。

  • デプロイを「Test latest code」にしたまま、下のインストールを選択すると自分のアカウントで利用できるようになります。

  • 適当なアプリを開くとデプロイできています。

処理を実装する

buildCard() の修正

  • まず、GPTからの返答を受け取って修正するために、 buildCard に改修を加え、レスポンスを表示できるようにします。
// 引数にレスポンス内容を入れられるようにする(デフォルトは空)
function buildCard(response = '') {
    let cardSection1TextInput1 = CardService.newTextInput()
        .setFieldName('message')
        .setTitle('GPTに聞く内容')
        .setMultiline(true);

    let cardSection1ButtonList1Button1Action1 = CardService.newAction()
        .setFunctionName('send')
        .setParameters({});

    let cardSection1ButtonList1Button1 = CardService.newTextButton()
        .setText('送信する')
        .setTextButtonStyle(CardService.TextButtonStyle.TEXT)
        .setOnClickAction(cardSection1ButtonList1Button1Action1);

    let cardSection1ButtonList1 = CardService.newButtonSet()
        .addButton(cardSection1ButtonList1Button1);

    let cardSection1DecoratedText1 = CardService.newDecoratedText()
        .setTopLabel('GPTからの返答')
        .setText(response); // <- ここにレスポンスをセットする

    let cardSection1 = CardService.newCardSection()
        .addWidget(cardSection1TextInput1)
        .addWidget(cardSection1ButtonList1)
        .addWidget(cardSection1DecoratedText1);

    let card = CardService.newCardBuilder()
        .addSection(cardSection1)
        .build();
    return card;
}

OpenAI の API KEY を登録する

  • 次に、Open AI の API KEY をスクリプトプロパティに登録します。
  • 設定のスクリプトプロパティに OPENAI_API_KEYというプロパティ名でキーを保存します。

コールバック関数の作成

  • 先ほどのUIを出力するコードの下記で send というコールバックを指定しました。これの中身を作成していきます。
  • send 関数はイベントオブジェクトを一つ取り、その中にフォームの中身などが入っています。今回は fieldName に message を指定したので以下のようになります。
    • レスポンスは newActionResponseBuilder()buildCard() を渡してアップデートさせています。
// OpenAI の /chat/completions を叩く関数
function getCompletions_(message) {
  // API KEY の取得
  const properties = PropertiesService.getScriptProperties();
  const apiKey = properties.getProperty('OPENAI_API_KEY');

  const path = `/chat/completions`;
  const payload = {
    model: 'gpt-3.5-turbo',
    messages: [{ role: 'user', content: message }],
    max_tokens: 256,
    temperature: 0.7,
  };

  const response = UrlFetchApp.fetch(`https://api.openai.com/v1${path}`, {
    method: 'post',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    payload: JSON.stringify(payload),
  });

  const json = JSON.parse(response.getContentText());
  return json.choices[0].message.content;
}

function send(e) {
  // フォームの取得
  const message = e.commonEventObject.formInputs.message.stringInputs.value[0];

  // APIリクエストの実行
  const content = getCompletions_(message);

  // レスポンスの構築
  return CardService.newActionResponseBuilder()
    .setNavigation(CardService.newNavigation().updateCard(buildCard(content)))
    .setStateChanged(true)
    .build();
}
  • アプリをリロードした上で、適当な文字列を入れて送信するをクリックすると返答が表示されます。

公開する

  • 公開するには、下記の手順を踏む必要があります。

デプロイを作成する

  • デプロイから、新しいデプロイを選択します。
  • GCP プロジェクトに紐つけていないのでデプロイできませんと言われるので、プロジェクトの種類を変更し、新しく作成したGCPプロジェクトに紐つけます。

  • 紐つけた後はデプロイが行えるので再度デプロイを行い、デプロイIDをコピーします。

ストアへの公開設定

  • GCPプロジェクトの方では OAuth 同意画面を作成した後、APIとサービスからGoogle Workspace Marketplace SDK を有効にします。

  • Google Workspace Marketplace SDK の「アプリの構成」で以下の設定をします。
    • アプリの公開設定は「限定公開」
    • アプリの統合は Google Workspace アドオンにチェックし、先ほどコピーしたデプロイIDを入力する。
    • OAuthスコープにマニフェストファイルの oauthScopes で設定したスコープをすべて入力する。
    • デベロッパー情報を入力する。

  • さらに、「ストアの掲載情報」を入力します。アイコンの設定などがめんどくさいですが必須です。

  • 公開を押すと、内部アプリとして公開され、ユーザーが自由にインストールできるようになります。

まとめ

  • Google Workspace Addon の内部向けの作成・公開方法をざっくり解説しました。
  • コンテキストインタフェースを使うとさらにイベントに応じたUIを出したりできるので、ぜひ社内DXの選択肢に入れてみてください。