AI
2024/11/07
上田 裕大

Difyと公開範囲を組織内に絞ったGASの接続

今回はDifyとスプレッドシートを接続して、出力をスプレッドシートに入れるアプリケーションを作成しました。その際、スプレッドシートの公開範囲が組織内のみに絞られているような場合で追加の処理が必要だったので、その内容をこちらでまとめます。

はじめに

今回、私は知識データベースを作成した上で、それをクエリと合わせてLLMに入れ、その出力をスプレッドシートに入れるというワークフローを作成しました。その際に、スプレッドシートとの接続の部分で苦労したところがあるので、こちらでまとめようと思います。

なお、技術ブログを見るとコンテナで開発されている方も多いと思うのですが、技術検証からスタートということもあり、簡素に作成できるクラウド版のサービスを用いて開発をしております。

Difyとは

Difyとは、複雑なワークフローであっても生成系AI関連のAPIを直感的にわかりやすいブロックを組み合わせてアプリケーションを作成できるツールです。以下の写真はアプリケーション内で公開されているテンプレートの1つですが、このようにフローチャートの様な形でワークフローを組めるので、エンジニアリングスキルがあまりない方でも比較的楽に使えるものとなります。近年では急速に注目を集めており、日々アップデートがされていてできることも増えてきています。


Difyはクラウド版のサービスが展開されており複数の有料プラン含めて展開されていますが、同時にソースコードも公開されており、自分で建てたコンテナや仮想マシンの中で動かすこともできます。実際に使用する場合にはコストや用途に応じて、いずれかを選ぶことになります。冒頭にも述べたように、今回はクラウド版を使用しています。

Difyの強み

Difyの優れているところは、なんといっても様々な生成系AIのサービスを同じようなUIで視覚的にわかりやすい形でまとめられていて、APIを呼び出すための様々な処理が隠蔽されていることでしょう。1から作ろうとすると、それぞれのAPIの仕様を見ながら、リクエストを送信する必要がありますし、Langchainのような複数のAPIを統合してくれる様なツールは使用するためのハードルが低いとは言えず、エンジニアリングスキルを必要とします。
Difyから見れば OpenAIのchatGPTであってもAnthropicのClaudeでも、(APIキーがあればですが) その差を意識することなく同じブロックとして扱えて、手で書くときには意識しなければならない形式なども考えなくて良いです。また、RAGを使ったり、条件分岐をしたりといったフローの処理についてもブロックを繋ぐだけで、簡単にできるというのが大変良いところです。

Difyの弱み

執筆している2024年10月現在では、Dify側でセキュリティをかけることができません。デプロイして公開したら最後、全ての人がアクセスできる様になってしまいます。ウェブアプリケーションとして公開していると、そのリンクを知っている全ての人が実行できてしまいます。
もちろん、一般公開を目的とする様なアプリケーションであれば問題ありませんが、社内用のツールなど、特定のユーザーのみに公開を限定したいケースもあります。Difyのアプリケーションでは有料のAPIを使用していることがほとんどなので、その様な場合には使用を想定していないユーザーに不正に使用され続けてしまうことになります。
また、一般公開している場合でもDifyだけではユーザー側に無制限で利用を認めてしまうことになります。これを防ぐには、Dify以外のツールを組み合わせてDifyにリクエストが飛ぶ前段階で制御する必要があるようです。

また、入出力における外部APIとの接続のサポートがLLMなどと比べて十分進んでいないという問題もあります。おそらく、ある程度時間が解決する問題かとも思いますが、現状では入力としてHTTPリクエスト、出力としてHTTPリクエストやslackへの送信などしかできなくなっています。エンジニア目線だと、これで十分と言えば十分なのですが、気軽にできるというDifyの優位性を考えると改善が望まれる部分かなと思います。

今回やったこと

今回、私がDifyを使用した目的は社内ツールの開発に向けたPoCであり、ワークフローの出力をスプレッドシートに入れるというものでした。あくまでPoCということで、無料のツールであるGoogle Apps Script (GAS)のみを使いました。
他の技術記事等でもDify→GASの処理というのは行われているのですが、これらの中ではGASを誰でも実行可能な形でデプロイしています。問題になったのはこのGAS側の認証の部分です。弊社のGoogleアカウントの運用上、GASを誰でも実行可能な状態にはしたくないというものがあり、紹介されている方法をそのまま利用するには至りませんでした。
そこで、今回はアクセストークンを発行するプロセスをワークフローに追加し、それをDifyからのリクエストに付与することによって対応しました。GASの実行を組織内のみに留めたいというケースは弊社に限るものではないと思いますので、こちらで具体的な方法をまとめます

全体の流れ

今回私が実装したDifyからGASを使ってスプレッドシートに出力を入れていく過程は、以下のようになります。(画像は参考で、LLMによる出力生成部分は本筋では無いので省略しています。)


1. DifyでLLMなどを組み合わせて出力を得る
2. DifyでGASのアクセストークンを生成
3. DifyからGASへHTTPリクエストを送信
4. GASで受け取ってスプレッドシートへ追加
5. リクエストのステータスコードをもとにDifyで処理

本記事でメインとなるのは2と3の部分になります。また、1と5については触れません。

従来の方法の問題点

GAS側で特定のユーザーのみしか実行できないような制約がある場合に、Difyから送信したリクエストがGAS側の権限問題で弾かれてしまい、そもそも実行されないという問題があります。GASの実行自体がトリガーしないので、当然GAS側で解決することはできず、GASの外側で実行する必要があります。
そこで、今回はGCPのOAuth2.0クライアントを使用してアクセストークンを発行し、リクエストに付与することによって組織内のユーザーとしてGASにアクセスしました。
まず、4の部分に軽く触れた上で、2,3について説明します。

GASによるスプレッドシートへの追加処理

まず、Difyからデータを入れる先のスプレッドシート側で準備をします。
追加したいスプレッドシートに対して、拡張機能 > Apps ScriptでGASを開きます。
今回のDifyの実装では、Contentという属性の中に文字列の形でDifyの出力を格納しておきます。その上で、一連のワークフローが動いた際の時刻とともにスプレッドシートに追記していきます。この辺りは、Difyの出力についての実装に合わせて調整してください。


function doPost(e) {
Logger.log("start function");
try {
// リクエストのJSONデータを解析
var jsonData = e['Content'];
Logger.log("jsonData" + jsonData);

// 現在のスプレッドシートを取得し、シート名を指定
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('[スプレッドシートの名前]');
if (!sheet) {
throw new Error("Sheet '[スプレッドシートの名前]' is not found.");
}

// 現在の日時を取得
var currentDate = new Date();

// データを格納するオブジェクト
var data = {
timestamp: currentDate,
content: jsonData || ''
};

// スプレッドシートに新しい行を追加
sheet.appendRow([data.timestamp, data.content]);

// 成功ステータスを返す
return ContentService.createTextOutput(JSON.stringify({status: 'success'}))
.setMimeType(ContentService.MimeType.JSON);

} catch (error) {
Logger.log("error occured.");
Logger.log(error.toString())
// エラー発生時の処理
return ContentService.createTextOutput(JSON.stringify({status: 'error', message: error.toString()}))
.setMimeType(ContentService.MimeType.JSON);
}
}


その上で、実行可能APIとしてデプロイします。今回はセキュリティの要件上、GASを全ての人に対して公開することはせずに、組織内のユーザーのみに限定してデプロイします。この時のURLはDify側で使用するので、保存しておいてください。

アクセストークンの発行

アクセストークンとは、特定の認証済みの対象の権限としてアクセスするためのものです。今回は認証済みの対象として、組織のGCPで作成したOAuth2.0クライアントを使用します。これにより、GASに対して組織内の実体としてアクセスできます。
なお、アクセストークンは強い権限を持ったものなので、通常一定時間で失効してしまう形となっています。これを再発行するためには、リフレッシュトークンというものが必要になります。今回は、リフレッシュトークンを作成しておき、ワークフローが実行されるたびにリフレッシュトークンからアクセストークンを作成する形にしています。
OAuth2.0クライアントの発行→リフレッシュトークンの作成→Difyワークフローでのアクセストークンの発行の順に説明します。

OAuth2.0クライアントの発行
GCPのプロジェクトにアクセスした上で、OAuth2.0クライアントを作成します。具体的な作り方については、過去に記事にしているのでこちらの記事を参考にしてください。承認済みのリダイレクトURIについては、リフレッシュトークンを作成する際に設定しますので、ここでは保存しておき後に回します。
その上で、クライアントシークレットを発行し、ダウンロードします。こちらについてもリフレッシュトークン作成時に必要になります。

リフレッシュトークンの作成
こちらの記事で紹介されているアプリケーションを使用すると、リフレッシュトークンを発行することができます。具体的な処理手順は記事を見ていただきたいのですが、アプリケーションのスプレッドシートには、OAuth2.0クライアントのシークレットについての情報とスプレッドシートに紐づいたGASの情報を入れていきます。
大事なのは、リフレッシュトークンはOAuth2.0クライアントに紐づく形で定義されているので、スプレッドシート等には依存しないものということです。よって、この過程では記事で紹介されているアプリケーションのGASのみを使用し、Difyとスプレッドシートを繋ぐGASは一切使用しない、ということに注意してください。

ワークフローでアクセストークンを発行
ワークフローの中に、先のアプリケーションで取得したリフレッシュトークンとOAuth2.0アカウントのクライアントID、クライアントシークレットを入れておき、それを使用して以下の部分でアクセストークンにします。リフレッシュトークンなどはセキュリティに気をつけるべき情報ですので、Difyの環境変数にシークレットとして保存しておきましょう。


Access Tokenの発行ブロックでは、HTTP POSTメソッドを選択し、送信エンドポイントとしてhttps://oauth2.googleapis.com/tokenを指定します。HTTPリクエストのボディについては以下の画像を参考に、”x-www-form-urlencoded”形式でOAuth2.0クライアントのクライアントIDとシークレット、先の過程で取得したリフレッシュトークンを入れます。


これに対するレスポンスとしてアクセストークンを取得できます。レスポンスの形は一定ですのでナイーブな実装になりますが、ボディ部分を引数として取って以下のような形で実装可能です。


def main(arg1: str) -> dict:
print(arg1)
arg2 = arg1.split(',')[0]
arg3 = arg2.split('"')[-2]
access_token = arg3.replace("\\", "")
return {
"result": access_token,
}


これでアクセストークンが取得できました。

DifyからGASへのHTTPリクエストを投げる

最後に、生成したアクセストークンからGASへアクセスするリクエストを生成します。
まず、送信先のGASでデプロイした際に生成されたURL (実行可能APIとしてデプロイできていれば、リンクの末尾は`:run`になっているはずです)を画像の1の部分に貼り付けます。


その上で、2の部分を選択すると認証についてのポップアウトが出てきますので、APIキー→Bearerを選択した上で、APIキーとして前のブロックから抽出したアクセストークンを入れます。以上で、Difyの出力が組織内のユーザーしか触れないスプレッドシートまで入れられるようになりました。

終わりに

今回はGASやスプレッドシートの公開範囲を組織内に絞った上で、Difyの出力のスプレッドシートへの掃き出しを行いました。もちろん、この状態でもスプレッドシート側にアクセスできる人を限定できたに過ぎず、Dify側のセキュリティが担保されたわけではありません。ただ、1つの要素のアクセスできる範囲を限定することは有用ですし、社内ツールの開発などではこれで十分なケースもあるかと思います。何かしら参考になれば幸いです。

参考記事

New call-to-action