📌

【ど素人が作る】Dify×Slackで就業規則ChatBotを作ってみた

に公開

初めに

「賞与っていつ貰えるのかな?」
「有給休暇っていつから付与されるんだ?」
「バナナは福利厚生に入るかな?」
など、会社のルールについてちょっと調べたいけど、就業規則を読むのだるいと思ったことはありませんか?

そんなあるあるを解決すべく、社内利用しているSlackにメンションするだけで、AIが回答してくれるようなボットを作った話です。

なお、筆者はコードを書くことがど素人なので、そんな方にもできるような記事にしていきたいと思います!

参考記事

まず初めに参考となる記事を載せます。これによると、DifyとGASがあればノーコードで開発ができそうなので、これに従って進めることにします!
https://note.com/samuraijuku_biz/n/n13c56a46fcaf

準備物

  • Difyアカウント(社内にあったもの)
  • Slack(社内にあったもの)
  • Googleアカウント(無料)

準備物はたったのこれだけ!それでは作ってみましょう!!

ステップ1:Difyの設定

ナレッジに就業規則の登録

Difyにログインしたら、上部メニューバーから「ナレッジ」を選択し、「ナレッジを作成」を押しましょう

今回、弊社の就業規則は.pdfファイルなので、「テキストファイルからインポート」を選びます。

あとは基本的に推奨を選んでおけば大体の設定はOK!

保存に成功すると以下のような画面になります!
これでナレッジの登録が完了です!

アプリの作成

上部メニューバーから「スタジオ」を選択し、「最初から作成」を押しましょう

「チャットボット」を選択し、適当にアプリのアイコンと名前と説明を書きましょう。

手順の箇所に以下のプロンプトを入力します。

<instruction>
ユーザからの質問に対して、コンテキストから該当箇所を抜き出してください。その情報を用いて、質問に対する最適な回答を作成してください。XMLタグは出力に含めないでください。

手順は以下の通りです:
1. ユーザの質問を理解し、それに関連するキーワードやフレーズを特定します。
2. コンテキストを検索し、質問に関連する情報を見つけます。
3. 見つけた情報を元に、ユーザの質問に対する最適な回答を作成します。

<example>
例えば、ユーザの質問が「地球は何歳ですか?」で、コンテキストに「地球は約45億年前に形成された」という情報がある場合、回答は「地球は約45億年前に形成されたため、地球の年齢は約45億歳です。」となります。
</example>
</instruction>

さらに下の「コンテキスト」から先ほど登録したナレッジを登録します。

するとこうなります。

あとは、画面右上にある、「公開する」から「更新」を押しましょう!
これで最低限のチャットボットが完成しました!簡単ですね。

Slackに連携するためにAPIキーを発行する

先ほどの「公開する」から「APIリファレンスにアクセス」を押します。
するとAPIアクセスに関する画面に遷移します。画面右上にある「APIキー」を押します。

ここで新しいシークレットキーを作成できるので、作成してください。
後で使うので、これはこのままメモしておいてください。
忘れてしまった場合も、同じ箇所からまたコピーできるのでご安心を。

これでDifyに関する設定は以上です!!!

ステップ2:Slackの設定

Slack側のでのアプリを作っていきます。

https://api.slack.com/apps
まずはここにアクセスして、「Create New App」を押しましょう。
ログインが求められる場合は、指示に従ってログインしてください。

以下の画面が表示されるので、下の「From scratch」を選びましょう。

アプリ名とアプリを作りたいワークスペースを選びます。
アプリ名はなんでも良いです。

まず、左メニューから「OAuth&Permissions」を選択し、下の方にある「Scoprs」に以下の権限を与えましょう。

次に左メニューから「App Home」を選択し、表示名を編集しましょう。

最後に左メニューから「Install App」を選択し、インストールしてtokenを吐かせましょう!
このtokenも後で使うので控えておいてください。

これでSlack側の設定は以上です!

ステップ3:GASの設定

GAS実装

GAS(Google Apps Script)を使って、DifyとSlackを繋ぎます。
もう一息です!頑張って!!

以下のURLからGASにアクセスします。
https://script.google.com/home

「新しいプロジェクト」を選択し、エディタを開きましょう。

ここで、コードが書けない私は「うっ...」となりましたが、ご安心を。
AIに書かせてしまえばいいのです!なんて世の中になったものだ。。。
私はClaude Sonnet 4で書かせることにしました。

Claude:
https://claude.ai/new

こんな感じで参考記事を元にプロンプトを書きました。

あなたはGoogle Apps Scriptを書くのが得意な優秀なプログラマーです。
Slackのスレッド内のメンションイベントをトリガーとして送信されるHTTPリクエストを拾ってDifyのフローに、メンションに書かれた指示を送って、処理結果を受け取って、Slackのスレッドに返すGASのコードを書いてください!
# DifyのインプットとアウトプットのJsonの項目
- メンションに書かれた指示:inputs.instruction
- 回答:data.outputs.outputText
# 注意事項
- Slackが外部APIを呼び出す場合は、3秒以内にレスポンスがないとリトライされてしまうので、リトライ分のリクエストは処理しないようにキャッシュを使って対応してください
- SlackのEvent APIにおけるURL検証リクエストは、リクエストボディにJSON形式で challenge パラメータが含まれて送信されます
- APIキーなどはスクリプトプロパティに登録する仕様にしてください
- コードに対するコメントは日本語でお願いします

するとみるみるコードを書いてくれます。
おっと、ここでテスト関数も出してみてほしいですね。

Difyと接続テストをする関数も実装してください。

するとあっという間に直してくれます。

出来上がったコードはこちら。

/**
 * Slackのメンションイベントを受信し、DifyのフローAPIを呼び出してSlackに返信するGAS
 */

// スクリプトプロパティから設定値を取得
const SLACK_BOT_TOKEN = PropertiesService.getScriptProperties().getProperty('SLACK_BOT_TOKEN');
const DIFY_API_URL = PropertiesService.getScriptProperties().getProperty('DIFY_API_URL');
const DIFY_API_KEY = PropertiesService.getScriptProperties().getProperty('DIFY_API_KEY');

/**
 * SlackのEvent APIからのHTTPリクエストを処理するメイン関数
 * @param {Object} e - HTTPリクエストイベント
 * @return {Object} - HTTPレスポンス
 */
function doPost(e) {
  try {
    // リクエストボディをパース
    const requestBody = JSON.parse(e.postData.contents);
    
    // URL検証リクエストの場合はchallengeを返す
    if (requestBody.type === 'url_verification') {
      console.log('URL検証リクエストを受信');
      return ContentService
        .createTextOutput(requestBody.challenge)
        .setMimeType(ContentService.MimeType.TEXT);
    }
    
    // イベントコールバックの場合
    if (requestBody.type === 'event_callback') {
      const event = requestBody.event;
      
      // メンションイベントのみ処理
      if (event.type === 'app_mention') {
        // リトライ防止のためのキャッシュチェック
        const cacheKey = `slack_event_${event.client_msg_id || event.ts}`;
        const cache = CacheService.getScriptCache();
        
        if (cache.get(cacheKey)) {
          console.log('重複リクエストをスキップ:', cacheKey);
          return ContentService
            .createTextOutput('OK')
            .setMimeType(ContentService.MimeType.TEXT);
        }
        
        // キャッシュに保存(10分間)
        cache.put(cacheKey, 'processed', 600);
        
        // 非同期でDify APIを呼び出し、Slackに返信
        processSlackMention(event);
      }
    }
    
    // 正常なレスポンスを即座に返す(3秒以内)
    return ContentService
      .createTextOutput('OK')
      .setMimeType(ContentService.MimeType.TEXT);
      
  } catch (error) {
    console.error('doPost エラー:', error);
    return ContentService
      .createTextOutput('ERROR')
      .setMimeType(ContentService.MimeType.TEXT);
  }
}

/**
 * Slackのメンションイベントを処理する関数
 * @param {Object} event - Slackイベント
 */
function processSlackMention(event) {
  try {
    // メンションテキストからボット名を除去して指示を抽出
    const instruction = extractInstruction(event.text);
    
    if (!instruction) {
      console.log('指示が見つかりませんでした');
      return;
    }
    
    console.log('抽出された指示:', instruction);
    
    // DifyのフローAPIを呼び出し
    const difyResponse = callDifyFlow(instruction);
    
    let outputText = null;
    
    // ワークフローAPIの場合
    if (difyResponse && difyResponse.data && difyResponse.data.outputs && difyResponse.data.outputs.outputText) {
      outputText = difyResponse.data.outputs.outputText;
    }
    // チャットボットAPIの場合
    else if (difyResponse && difyResponse.answer) {
      outputText = difyResponse.answer;
    }
    
    if (outputText) {
      // Slackのスレッドに返信
      postToSlackThread(event.channel, event.ts, outputText);
    } else {
      console.error('Difyからの応答が不正です:', difyResponse);
      postToSlackThread(event.channel, event.ts, 'エラー: 処理に失敗しました。');
    }
    
  } catch (error) {
    console.error('processSlackMention エラー:', error);
    // エラー時もSlackに通知
    if (event.channel && event.ts) {
      postToSlackThread(event.channel, event.ts, 'エラーが発生しました。しばらく後にもう一度お試しください。');
    }
  }
}

/**
 * メンションテキストから指示部分を抽出
 * @param {string} text - メンションテキスト
 * @return {string} - 抽出された指示
 */
function extractInstruction(text) {
  // <@UXXXXXXXX> の形式のメンションを除去
  const instruction = text.replace(/<@[UW][A-Z0-9]+>/g, '').trim();
  return instruction;
}

/**
 * DifyのAPIを呼び出す(ワークフローまたはチャットボット対応)
 * @param {string} instruction - 指示内容
 * @return {Object} - Difyからのレスポンス
 */
function callDifyFlow(instruction) {
  try {
    // まずワークフローAPI形式で試行
    let payload = {
      inputs: {
        instruction: instruction
      },
      response_mode: 'blocking',
      user: 'slack-user'
    };
    
    let options = {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${DIFY_API_KEY}`,
        'Content-Type': 'application/json'
      },
      payload: JSON.stringify(payload),
      muteHttpExceptions: true // エラーレスポンスの詳細を取得
    };
    
    console.log('Dify API(ワークフロー形式)を呼び出し中...');
    let response = UrlFetchApp.fetch(DIFY_API_URL, options);
    let responseData = JSON.parse(response.getContentText());
    
    // ワークフローAPIが成功した場合
    if (response.getResponseCode() === 200) {
      console.log('Dify APIレスポンス(ワークフロー):', responseData);
      return responseData;
    }
    
    // queryパラメータエラーの場合、チャットボットAPI形式で再試行
    if (response.getResponseCode() === 400 && 
        responseData.code === 'invalid_param' && 
        responseData.params === 'query') {
      
      console.log('チャットボットAPI形式で再試行...');
      
      payload = {
        inputs: {
          instruction: instruction
        },
        query: instruction, // チャットボットAPIの場合はqueryが必要
        response_mode: 'blocking',
        conversation_id: '',
        user: 'slack-user'
      };
      
      options.payload = JSON.stringify(payload);
      
      response = UrlFetchApp.fetch(DIFY_API_URL, options);
      responseData = JSON.parse(response.getContentText());
      
      if (response.getResponseCode() === 200) {
        console.log('Dify APIレスポンス(チャットボット):', responseData);
        return responseData;
      }
    }
    
    // どちらも失敗した場合
    console.error('Dify API呼び出し失敗:', {
      statusCode: response.getResponseCode(),
      response: responseData
    });
    throw new Error(`Dify API Error: ${responseData.message || 'Unknown error'}`);
    
  } catch (error) {
    console.error('Dify API呼び出しエラー:', error);
    throw error;
  }
}

/**
 * Slackのスレッドに返信を投稿
 * @param {string} channel - チャンネルID
 * @param {string} threadTs - スレッドのタイムスタンプ
 * @param {string} text - 返信内容
 */
function postToSlackThread(channel, threadTs, text) {
  try {
    const payload = {
      channel: channel,
      thread_ts: threadTs,
      text: text
    };
    
    const options = {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
        'Content-Type': 'application/json'
      },
      payload: JSON.stringify(payload)
    };
    
    const response = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', options);
    const responseData = JSON.parse(response.getContentText());
    
    if (responseData.ok) {
      console.log('Slackへの返信が成功しました');
    } else {
      console.error('Slackへの返信エラー:', responseData.error);
    }
    
  } catch (error) {
    console.error('Slack投稿エラー:', error);
  }
}

/**
 * 手動テスト用の関数
 * スクリプトプロパティの設定確認に使用
 */
function testConfiguration() {
  console.log('=== 設定確認 ===');
  console.log('SLACK_BOT_TOKEN:', SLACK_BOT_TOKEN ? '設定済み' : '未設定');
  console.log('DIFY_API_URL:', DIFY_API_URL ? '設定済み' : '未設定');
  console.log('DIFY_API_KEY:', DIFY_API_KEY ? '設定済み' : '未設定');
  
  if (!SLACK_BOT_TOKEN || !DIFY_API_URL || !DIFY_API_KEY) {
    console.log('⚠️ スクリプトプロパティの設定が不完全です');
    console.log('以下の項目をスクリプトプロパティに設定してください:');
    console.log('- SLACK_BOT_TOKEN: SlackボットのOAuthトークン');
    console.log('- DIFY_API_URL: DifyのフローAPIのURL');
    console.log('- DIFY_API_KEY: DifyのAPIキー');
  } else {
    console.log('✅ 設定は正常です');
  }
}

/**
 * 手動テスト用のDify API呼び出し関数
 */
function testDifyAPI() {
  try {
    const testInstruction = "テスト用の指示です";
    const response = callDifyFlow(testInstruction);
    console.log('Difyテスト結果:', response);
  } catch (error) {
    console.error('Difyテストエラー:', error);
  }
}

次に環境変数を設定します。
左の歯車メニューを選択し、下の方にある「スクリプト プロパティ」に環境変数を3つ追加します。

DIFY_API_KEY:ステップ1で控えたDifyのシークレットキー
DIFY_API_URL:DifyのベースURL+/chat-messeges ※ここ注意
SLACK_BOT_TOKEN:ステップ2で控えたSlackのToken

※このDIFY_API_URLに私は苦戦しました。Difyのアプリの種類によって、ベースURLに続くURLが違うのです。
まずベースURLとは、Difyでシークレットキーを払い出す画面にちらっと写っていた。ここのURLです。

これに続くURLとして、アプリの種類によってURLが変わるそうです。

  • チャットアシスタント → /v1/chat-messages
  • Agent → /v1/chat-messages
  • テキスト生成 → /v1/completion-messages
  • ワークフロー → /v1/workflows/run

間違えないように気をつけましょう。今回は、チャットアシスタントを使用しているので、/v1/chat-messagesでよかったです。探すの苦労しました。

念の為、テスト関数を実行してみましょう!
エディタに戻り、画面上部の赤枠の箇所からtestDifyAPIを選んで実行するだけです!
エラーが出なければ成功です!

何かしらエラーが出ても、それをAIにコピペして聞けば勝手にコード修正してくれます。すごいですよね。

デプロイ

実装が完了したら、デプロイします。
画面右上にある「デプロイ」ボタンから「新しいデプロイ」を選択します。

「種類の選択」の横にある歯車マークから「ウェブアプリ」を選択します。
アクセスできるユーザを「全員」にしてください。

「デプロイボタン」を押せば、このアプリが公開されます!
このとき、URLを控えておいてください。

デプロイしたURLをSlackに連携する

Slack apiの画面に戻ります。
左側メニューから「Event Subscriptions」を選択し、Enable EventsをONにします。
Request URLに先ほどデプロイしたGASのURLを貼り付ければOKです!
また、下の方にある、Subscribe to bot eventsに、「app_mention」を加えておくのを忘れずにお願いします。

以上で実装終了です!お疲れ様でした!

ステップ4:使ってみる

実際にSlackからチャットボットにアクセスできるか試してみましょう。
チャットボットを呼びたいチャンネルの詳細を開き、インテグレーションタブから今作ったアプリを追加するだけです!

メンションで指定してあげるだけで、回答が返ってくるようになりました!!!やったね!!

最後に

Google Apps ScriptとSlack、Difyを組み合わせることで、社内の知識検索を大幅に効率化できました。

Difyは触ってみると面白く、他にも最近のトレンドの記事を5つ紹介してくれるBotなど、複数作ってみました。発想次第でたくさんの便利アプリを作れると思います。

ぜひみなさんも、社内の問題をDify×Slackで解決してみませんか??

もしもこの記事が良いと思ったらいいねお願いします!

うぐいすソリューションズTechBlog

Discussion