Loading
BLOG 開発者ブログ

2023年6月2日

Azure FunctionsとAzure OpenAI Serviceを活用した効率的なSlack botの作成手順

Azure FunctionsとAzure OpenAI Serviceを活用した効率的なSlack botの作成手順

今年に入ってから大きな盛り上がりを見せているChatGPT。
そしてこれをAzure上でホストし、セキュリティ/プライバシー保護の面で強みがあるのがAzure OpenAI Serviceです。


そこで、今回はAzure FunctionsとAzure OpenAI Serviceを組み合わせたSlack botの作成手順を詳しく解説します。SlackにAzure OpenAI Serviceを導入することで、より効率的かつ楽しいコミュニケーションを実現しましょう!

目次


はじめに

こんにちは。
クラウドソリューショングループのwatanabe.tです。


OpenAIからChatGPTが発表された直後から便利に使っていたのですが、せっかくなので社内コミュニケーションにも寄与できたらと思い、ChatGPT(Azure OpenAI Service)と連携するSlack botを作成し、社内Slackに導入しました。

なお、導入にあたり下記ページを参考にさせていただきました。ありがとうございました。
Azure FunctionsとChatGPT APIで作ったSlack Botをコンテキスト対応しました | 株式会社ジェイテックジャパン

ちなみに、Node.jsからChatGPTを利用するためのライブラリである openai-node は公式にはAzure OpenAI Serviceに対応していませんが、 Issueの中で 対応方法が回答されています。

前提条件

今回の手順を実施する上で、以下は実施済みであり解説を行いません。

  • Slackにアプリを追加する権限を有していること
  • Azureのサブスクリプションが作成済みであること
  • 上記サブスクリプション配下にリソースグループが作成済みであること
  • コードを管理するGitHubリポジトリが作成済みであること


また、Slack botは以下の技術要素/仕様で作成しました。
細かい用語の説明は省きますので、各自調べていただければと思います。

  • Slack botはAzure Functions上で動作する
  • Azure OpenAI Serviceのgpt-35-turboモデルを利用する
  • 推論に時間がかかるため、Azure Functionsは非同期処理
  • SlackのEvent SubscriptionsでAzure Functionsを呼び出し、Incoming Webhooksに返信する
  • DeployはGitHub Actionsから行う


Slack botの基本設定

まずは、Slack apiのAppsページを開き、「Create an App(既にAppが存在する場合はCreate New App)」からアプリを作成します。
今回はコードも全て用意しているため、「From scratch」を選択し、イチから作成していきましょう。

「App Name」にbot名を入力し、導入するWorkspaceを選択したら、「Create App」でbot用のSlack Appが作成されます。

次に「Incoming Webhooks」を有効化します。
「Add features and functionality」セクションの「Incoming Webhooks」を選択し、トグルスイッチを On にします。

さらに「OAuth & Permissions」からこのbotの権限を設定します。
「Scopes」セクションの「Bot Token Scopes」の方の「Add an OAuth Scope」を選択し、 app_mentions:read channels:history channels:read chat:write の4つを選択します。(incoming-webhookは既に有効になっていると思います。)

さらに「Install to Workspace」を選択し、チャンネル設定後、表示される Bot User OAuth Token を控えておきます。

最後に「Event Subscription」でbotからAzure Functionsを呼び出せるようにします。
「Event Subscriptions」セクションの「Enable Events」トグルスイッチを On にします。ここで「Request URL」を入力する必要がありますが、Azure Functions作成後に払い出されるエンドポイントを設定するので、一旦は空のままで問題ありません。

Azure OpenAI Serviceのセットアップ

ここでは、Azure OpenAI Serviceからgpt-35-turboのモデルをデプロイしていきます。
Azure Portalから「Azure OpenAI」を検索し、必要事項を入力して作成していきます。
サブスクリプションとリソースグループは事前に作成していたものを選択し、リージョンは East US 、価格レベルは Standard S0 を選択します。

作成完了後、作成したリソースを選択します。
「キーとエンドポイント」から「キー1」と「エンドポイント」を控えておきます。(後ほどFunctionsから呼び出すのに使います)

その後、「Azure OpenAI Studio に移動する」を選択し、Studioを起動します。
リソース作成直後はモデルが存在しないため、最初のデプロイを行います。
「デプロイ」から「新しいデプロイの作成」を選択します。
モデルは gpt-35-turbo を選択し、デプロイ名は任意のものにします。ここのデプロイ名を後ほど利用します。


Azure Functionsのセットアップ

いよいよSlack botの本体にあたるアプリを作っていきます。
Azure Portalから「Azure Functions」を検索し、「作成」を選択したのち、必要事項を入力して作成していきます。

項目名 設定値 備考
サブスクリプション 事前に作成したもの
リソースグループ 事前に作成したもの
関数アプリ名 ご自由に ドメインの一部になるため、世界で一意にする
コードまたはコンテナーイメージをデプロイしますか? コード
ランタイムスタック Node.js
バージョン 18 LTS
地域 Japan East 任意の地域でOK
オペレーティングシステム Windows
ホスティングプラン 消費量(サーバーレス) Slackから都度呼び出しなのでコストメリット有り
ストレージアカウント ご自由に アプリ本体が格納されるストレージ
パブリックアクセスを有効にする オン Slackからアクセスできる設定
Application Insightsを有効にする はい ログ収集用
継続的デプロイ 有効化 作成済みのGitHubリポジトリ、ブランチを選択


これでGitHubリポジトリにプッシュすると自動的にAzure Functionsにデプロイされるため、
以下のコードをリポジトリ内にindex.jsとして保存します。

なお、以下のコードでは、SlackやOpenAI ServiceのAPIキーは関数の環境変数に設定するようにしています。

const { WebClient } = require("@slack/web-api");
const {
  ChatCompletionRequestMessageRoleEnum,
  Configuration,
  OpenAIApi,
} = require("openai");

const openaiClient = new OpenAIApi(
  new Configuration({
    apiKey: process.env.OPENAI_API_KEY,
    basePath: process.env.OPENAI_API_URL + process.env.OPENAI_API_MODEL,
    baseOptions: {
      headers: {'api-key': process.env.OPENAI_API_KEY},
      params: {
        'api-version': '2023-03-15-preview'
      }
    }
  })
);
const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);
const GPT_BOT_USER_ID = process.env.GPT_BOT_USER_ID;
const CHAT_GPT_SYSTEM_PROMPT = process.env.CHAT_GPT_SYSTEM_PROMPT;
const GPT_THREAD_MAX_COUNT = process.env.GPT_THREAD_MAX_COUNT;

/**
 * Slackへメッセージを投稿する
 * @param {string} channel 投稿先のチャンネル
 * @param {string} text 投稿するメッセージ
 * @param {string} threadTs 投稿先がスレッドの場合の設定
 * @param {object} context Azure Functions のcontext
 */
const postMessage = async (channel, text, threadTs, context) => {
  await slackClient.chat.postMessage({
    channel: channel,
    text: text,
    thread_ts: threadTs,
  });
  context.log(text);
};

/**
 * ChatGPTからメッセージを受け取る
 * @param {string} messages 尋ねるメッセージ
 * @param {object} context Azure Functions のcontext
 * @returns content
 */
const createCompletion = async (messages, context) => {
  try {
    const response = await openaiClient.createChatCompletion({
      messages: messages,
      max_tokens: 800,
      temperature: 0.7,
      frequency_penalty: 0,
      presence_penalty: 0,
      top_p: 0.95,
    });
    return response.data.choices[0].message.content;
  } catch (err) {
    context.log.error(err);
    return err.response.statusText;
  }
};

module.exports = async function (context, req) {
  // Ignore retry requests
  if (req.headers["x-slack-retry-num"]) {
    context.log("Ignoring Retry request: " + req.headers["x-slack-retry-num"]);
    context.log(req.body);
    return {
      statusCode: 200,
      body: JSON.stringify({ message: "No need to resend" }),
    };
  }

  // Response slack challenge requests
  const body = eval(req.body);
  if (body.challenge) {
    context.log("Challenge: " + body.challenge);
    context.res = {
      body: body.challenge,
    };
    return;
  }

  context.log(req.body);
  const event = body.event;
  const threadTs = event?.thread_ts ?? event?.ts;
  if (event?.type === "app_mention") {
    try {
      const threadMessagesResponse = await slackClient.conversations.replies({
        channel: event.channel,
        ts: threadTs,
      });
      if (threadMessagesResponse.ok !== true) {
        await postMessage(
          event.channel,
          "[Bot]メッセージの取得に失敗しました。",
          threadTs,
          context
        );
        return;
      }
      const botMessages = threadMessagesResponse.messages
        .sort((a, b) => Number(a.ts) - Number(b.ts))
        .filter(
          (message) =>
            message.text.includes(GPT_BOT_USER_ID) ||
            message.user == GPT_BOT_USER_ID
        )
        .slice(GPT_THREAD_MAX_COUNT * -1)
        .map((m) => {
          const role = m.bot_id
            ? ChatCompletionRequestMessageRoleEnum.Assistant
            : ChatCompletionRequestMessageRoleEnum.User;
          return { role: role, content: m.text.replace(/]+>/g, "") };
        });
      if (botMessages.length < 1) {
        await postMessage(
          event.channel,
          "[Bot]質問メッセージが見つかりませんでした。@chatgptbot を付けて質問してみて下さい。",
          threadTs,
          context
        );
        return;
      }
      context.log(botMessages);
      var postMessages = [
        {
          role: ChatCompletionRequestMessageRoleEnum.System,
          content: CHAT_GPT_SYSTEM_PROMPT,
        },
        ...botMessages,
      ];
      const openaiResponse = await createCompletion(postMessages, context);
      if (openaiResponse == null || openaiResponse == "") {
        await postMessage(
          event.channel,
          "[Bot]ChatGPTから返信がありませんでした。この症状は、ChatGPTのサーバーの調子が悪い時に起こります。少し待って再度試してみて下さい。",
          threadTs,
          context
        );
        return { statusCode: 200 };
      }
      await postMessage(event.channel, openaiResponse, threadTs, context);
      context.log("ChatGPTBot function post message successfully.");
      return { statusCode: 200 };
    } catch (error) {
      context.log(
        await postMessage(
          event.channel,
          `Error happened: ${error}`,
          threadTs,
          context
        )
      );
    }
  }
  context.res = {
    status: 200,
  };
};

コード保存後、GitHubのmainブランチにコミット・プッシュすることで、自動ビルド・デプロイがGitHub Actions上で実行され、自動的にAzure Functions上にデプロイされます。

Slack botの統合と動作確認

ここまで来たらもう一息です。
まずはAzure Functionsの「構成」から環境変数を設定します。

名前 備考
CHAT_GPT_SYSTEM_PROMPT You are an excellent AI assistant Slack Bot named “もばそるちゃんBot”.
Please output your response message according to following format.
– bold: “*bold*”
– italic: “_italic_”
– strikethrough: “~strikethrough~”
– code: ” \`code\` ”
– link: “<https://slack.com|link text>”
– block: “\`\`\` code block \`\`\`”
– bulleted list: “* item1”
Be sure to include a space before and after the single quote in the sentence.
ex) word\`code\`word -> word \`code\` word And Answer in language user uses.
Let’s begin.
システムメッセージとして、アシスタントの動作を予め設定できる。
今回はbot名を予め与え、返答はSlackのフォーマットになるように指定している。
GPT_BOT_USER_ID Slack AppのメンバーID Slackで「このアプリについて」を選択し、名前を選択すると確認できる。
GPT_THREAD_MAX_COUNT 20 Slackのスレッドをいくつまで含めるか。ChatGPTトークン消費量に関連する。
OPENAI_API_KEY (32桁の文字列) Azure OpenAIリソース作成時に控えておいたキー。
OPENAI_API_MODEL 例)gpt-35-turbo Azure OpenAI Studioでデプロイしたモデル名。
OPENAI_API_URL 例)https://example.openai.azure.com/ Azure OpenAIリソース作成時に控えておいたエンドポイント。
SLACK_BOT_TOKEN 例)xoxb- Slack Appインストール時のBot User OAuth Token。

最後に、作成済みの関数から「関数のURLの取得」を選択し、そのURLをSlack Appの「Event Subscriptions」の「Request URL」に入力します。

これで無事にSlack botが起動し、bot宛にメンションをつけてメッセージを送ると、スレッドで返答があるはずです。


おわりに

今回は、SlackからAzure OpenAI経由でChatGPTを利用する方法を紹介しました。

情報を尋ねたときの正確性はまだまだですが、アイディア出しや雑談の相手としては十分に有用だと感じました。
さらに外部のデータ取り込みなどもできるようにする手もあったりするので、かなりチャットボットとしてはレベルが高いんじゃないかと思います。
特に、systemに色々な事前プロンプトを仕込んでおくことで、様々な振る舞いをさせることができるのは面白く、色々なシーンでの活用が見込まれますね。

便利な活用方法を見つけつつ、振り回されないようにはしたいなと感じるこの頃でした。



watanabe.tのブログ

クラウドソリューショングループ所属

昔はモバイルアプリ開発を、今はGCP, AWS, Firebaseなどのクラウド周りの提案/開発を行っているエンジニアです。

AWS認定ソリューションアーキテクト取得しました!