sugiii8's tech blog.
post: 2023-08-15
INDEX

Azure OpenAI, Cognitive Search, OpenAI, Add your dataを使って、slack Botを作った

thumbnail

以前のSlackBotに続き、Microsoft Azureサービスを使って社内ナレッジを喋るSlackBotを作ってみようということになりました。

!! 注意 !!

英語ドキュメントじゃないとAdd your dataで正しくデータを検索できない問題が発生しています。

  • 未解決です
  • 動きとしては、OpenAIのPlayGroundだと日本語ドキュメントも正しく検索できるがAdd your data API経由だと検索ができない(ヒットしない)状態になっています

上記問題は発生しているものの、SlackBotへのメンションから英語ドキュメント検索結果をSlackBotが回答する、という一連の疎通が完了したので一旦記事化しました。

設計

処理フロー

  • SlackBotにメンションする
    • メッセージ内容は社内ナレッジに関する何かを尋ねることを想定
  • SlackBotからAzure Functionを呼ぶ
  • Azure Functions内でAdd your data APIを呼ぶ
    • Slackで尋ねた内容をそのままプロンプトにする
  • 結果をSlackBotが回答する


構成

  • Slack Apps
  • Azure

以下サービスを使う

  • Functions
  • OpenAI(Add your dataとして)
  • Storage
  • Cognitive Search


Slack Apps作成

以下記事を参考にSlack Appを作成しメンションしたら処理実行されるように設定しておく
Slack API を使用してメッセージを投稿する
SlackBotへのメンションをトリガーにメール送信する方法

Azure Functions

スケルトン作成

以下記事を参考に初期ファイルを生成する
Visual Studio Code を使用して Azure Functions を開発する

言語は書き慣れているTypeScriptにしました。
できあがったファイルのindex.tsのHttpTrigger内に処理を記述します。

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { WebClient } from "@slack/web-api";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  // 生成時のサンプルコード消してここに処理を記述していく
};

export default httpTrigger;


Slackとの連携

Slack Appと連携するための準備として、Slack AppのEvent設定時に連携先と疎通確認をして認証をします。
そのためのコードをAzure Functions側に記述しておく必要があります。

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { WebClient } from "@slack/web-api";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {

  // Slack認証から来た内容からchallenge部分をそのまま返す
  // 厳密にはパラメータのtypeが"url_verification"のほうが良いかもしれない
  if (req.body.challenge) {
    context.res = {
      body: req.body.challenge,
    };
    return;
  }


};

export default httpTrigger;


Azure OpenAI

Azure OpenAI周りの連携設定

以下は後で別記事で記載します

  • Cognitive SearchとStorageの連携
  • Cognitive SearchとOpenAI(Add your data)の連携

Azure OpenAIのPlayGroundからプロンプトを記載して、想定通りの回答が来ることを確認します。
結果はOpenAIに依存するので、ある程度正しい回答を返ること・ある程度引用されたドキュメントが正しいことを確認します。

Azure OpenAI(Add your data)のAPI化

こちらの記事が詳しく書いているのでこちらを読んでください、あと公式ページも参照しました
Azure Open AIの「Add your data」をAPIとして使う!
Azure OpenAI Service の REST API リファレンス

API化パターンは方法が2つ

上記の記事の通りですがAdd your dataをAPI化するには2つ方法があります。

方法1: Completions Extensions APIを使う
デプロイされたOpenAIからCompletions Extensions用のAPIが生えるのでそれを利用する

  • メリット
    • Add your dataのAPIを直接叩くので構成がシンプルになる
  • デメリット
    • パラメータが通常より複雑になる
# 公式ページサンプル
curl -i -X POST YOUR_RESOURCE_NAME/openai/deployments/YOUR_DEPLOYMENT_NAME/extensions/chat/completions?api-version=2023-06-01-preview \
-H "Content-Type: application/json" \
-H "api-key: YOUR_API_KEY" \
-H "chatgpt_url: YOUR_RESOURCE_URL" \
-H "chatgpt_key: YOUR_API_KEY" \
-d \
'
{
    "temperature": 0,
    "max_tokens": 1000,
    "top_p": 1.0,
    "dataSources": [
        {
            "type": "AzureCognitiveSearch",
            "parameters": {
                "endpoint": "'YOUR_AZURE_COGNITIVE_SEARCH_ENDPOINT'",
                "key": "'YOUR_AZURE_COGNITIVE_SEARCH_KEY'",
                "indexName": "'YOUR_AZURE_COGNITIVE_SEARCH_INDEX_NAME'"
            }
        }
    ],
    "messages": [
        {
            "role": "user",
            "content": "What are the differences between Azure Machine Learning and Azure AI services?"
        }
    ]
}
'


方法2: 設定済Add your dataをWebアプリケーションとしてデプロイする
Azure Web Appsを使ってデプロイしWeb Appsから生えたAPIを叩く

  • メリット
    • Web Appsごとにエンドポイントが生えるので、検索するStorageを分けたりするときは良さそう
    • APIのパラメータがシンプルになる
  • デメリット
    • Web Appsを介してAdd your data処理を呼ぶので構成が複雑になる
    • 複雑になるので、開発・運用時の問題切り分けが難しくなる

一度 Webアプリケーションとしてデプロイする 方式を試したのですがうまく行かず原因も分からない状態になったのと、
構成はシンプルにしておいたほうが良い(まだお試しの段階なので変更しやすい構成にしたい)ということも合ったので Completions Extensions APIを使う 方式にしました。

APIをローカルから叩く

だいたいこのような感じのコード書いてローカル実行してAPIの疎通確認をしました。
curlじゃなくてコードを書いたのは後で本実装時にある程度コピペしたかったからです。

const createCompletionExtensionText = async () => {
  const completionParams = {
    dataSources: [
      {
        type: "AzureCognitiveSearch",
        parameters: {
          endpoint: "{Your Cognitive Search Endpoint}",
          key: "{Your Cognitive Search Key}",
          indexName: "{Your Cognitive Search Index Name}",
        },
      },
    ],
    messages: [
      {
        role: "system",
        content:
          "{Your System Prompt}",
      },
      {
        role: "user",
        content: "{Your Prompt}",
      },
    ],
    temperature: 0,
    top_p: 1,
    frequency_penalty: 0,
    presence_penalty: 0,
    max_tokens: 800,
    stop: null,
  };

  // Completions Extensions APIが使えるのは 2023-06-01-preview のみ
  const url =
    "{Your OpenAI Completions Extensions API Endpoint}?api-version=2023-06-01-preview";

  try {
    const result = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "api-key": "{Your OpenAI Api Key}",
        chatgpt_url:
          "{Your OpenAI Endpoint}?api-version=2023-06-01-preview", // Extensionsではなく通常のOpenAI APIのEndpointoを指定する
        chatgpt_key: "{Your OpenAI Api Key}",
      },
      body: JSON.stringify(completionParams),
    });
    const a = await result.json();
    const assistantRes = a.choices[0].messages;

    // テストなのでとりあえずconsoleに出力して確認する
    assistantRes.forEach((m) => console.log(m.content));

  } catch (error) {
    console.log(`error: ${error}`);
  }
};

createCompletionExtensionText();


Azure Functionsに実装

上記テストコードを元に、Azure Functionsに処理を実装しました。

確認

SlackBotに話しかけてある程度想定通りの回答が返ってくればOK。
ドキュメントが英語じゃないと正しく検索できない問題があるので引き続き調査が必要ですが、一連の疎通確認はできました。

  • 架空の人物の英語名リストのPDF(ドキュメントタイトル:List of Important People )をStorageに登録して、Slackからそれを問い合わせてみた

  • PDFに記載された人物リストを検索して回答することを確認できました。
  • [doc1] という文字列は、Add your data APIから回答とは別に、ドキュメントの参照元データを返してくるのでそれの文字列部分が表示されています。
  • 当然このままでは社内ナレッジ回答Botとしては使えないので、何か工夫でいけるのかAzure側でいずれ解決される兆しはあるのかは引き続き調査します。


ハマりポイント

Slack api 連続発火 レスポンス3秒問題

シンプルにSlackに閉じた問題ですが、SlackとAzure Functionsの連携確認中にAzure Functionsが複数回呼ばれる現象が発生してました。
これはSlack側の仕様でレスポンスが3秒を超えるとリトライ処理をするようになっていることが原因でした。
Slack Events APIの再送仕様と回避方法まとめ
本ツールは社内向けSlackBotということもあり、厳密にリトライ処理をする必要は無いのでリトライ自体を無視することにしました。

リトライ時はリクエストヘッダに x-slack-retry-num が付与されているので、それがあれば無視するようにしました。

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { WebClient } from "@slack/web-api";

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {

  // Slack認証から来た内容からchallenge部分をそのまま返す
  // 厳密にはパラメータのtypeが"url_verification"のほうが良いかもしれない
  if (req.body.challenge) {
    context.res = {
      body: req.body.challenge,
    };
    return;
  }

  // Slack Event APIからのリトライは無視
  if (req.headers["x-slack-retry-num"]) {
    context.res = {
      statusCode: 200,
      body: JSON.stringify({ message: "No need to resend" }),
    };
    return;
  }

};

export default httpTrigger;  


Add your data APIをどのように生やすか問題

生やすか は語弊がある(Completions Extensions APIは既に生えているので)のですが、前述の通り2パターンあってどちらにすべきか検討が必要でした。
初めにWebアプリケーションとしてデプロイするパターンで実装した際は疎通がうまくいきませんでした。(原因は分かっていません。)
上記2パターンあるがどちらもメリデメがあるというところも悩みポイントかなと思われます。

Add your dataが英語しか検索できない

これが最大の問題です。
自分が試した限り、Add your data API経由だと日本語ドキュメントは検索できませんでした。
要調査。

以上です。