CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

Bot Framework ComposerでAzure OpenAI Serviceと接続したBotアプリケーションを作ってみました。

こんにちは、CCCMKホールディングス TECH LABの三浦です。

近所の街路樹が紅葉していて「秋だなぁ」と思ったのですが、もうすぐ12月なんですよね。例年だったらもっと早く紅葉している気がします。なんだか今年は色々、いつもと違うリズムの一年になったように感じています。

さて、先日Bot Framework Composerに入門した話をご紹介しました。

techblog.cccmkhd.co.jp

その後も色々試しながらBot Framework Composerについて調べており、最近ようやく少しずつですが仕組みが理解出来てきました。そこで今回はMicrosoft Teamsから送ったメッセージをAzure BotからAzure OpenAI ServiceにAPIで送信し、返ってきたレスポンスをAzure Botから応答メッセージとしてMicrosoft Teamsに送る、という一連の流れをBot Framework Composerを使って実装する方法についてご紹介したいと思います。

今回作るシステムの全体像です。

Botの構成

Bot Framework ComposerはMicrosoftが提供するBot開発フレームワーク、"Bot Framework"をGUIを通じて利用し、Botアプリケーションを開発することが出来る開発環境です。Bot FrameworkにおけるBotアプリケーション開発では"Dialog"という概念が中心になります。また、会話を通じて変化するBotの状態を保持する仕組みとして"Memory"があります。

Dialogの構成要素

DialogはRecognizer,Trigger, Action, Generatorの4つの機能を持っています。それぞれについて、簡単にまとめてみます。この内容は以下を参考にしています。

learn.microsoft.com

  • Recognizer
    ユーザーのBotへの入力を受け取り、ユーザーが意図することを解釈する役割を持っています。抽出されたユーザーの意図は"Intent"としてDialogのTriggerとして利用することが出来ます。今回は意識して使用しなかったのですが、これを利用することで例えば一般的な会話をするBotから特定領域の専門知識について回答してくれるBotにモード切替をする、といったことに活用出来ると思います。

  • Trigger
    なんらかのイベントの発生をキャッチして、それに応じた処理を実行する役目を持っています。勘違いしやすいのですが、1つのイベントにTriggerが紐づけられる処理は1つだけのようです。もし1つのイベントを起点に複数の処理を発火させたい場合はその処理の中でなんらかの発火イベントを起こす必要があるようでした。

  • Action
    ActionはBot Framework Composerにおける処理の1単位で、Triggerによって実行される処理はActionを組み合わせて実装します。Actionにはたとえばユーザーの入力を受け取るものや、HTTPリクエストを送ったり、ユーザーにメッセージを送信するものがあります。

  • Generator
    ユーザーへの応答メッセージを、登録済みのTemplateや変数を使って生成する機能です。

Memory

Botの状態、たとえばこれまでの会話の履歴や現在のユーザーの情報などを記録する仕組みが"Memory"です。Memoryはその値を保持する期間を定めた"Scope"を決めることが出来ます。Memoryに保存されたデータは"property"と呼び、propertyへの値の格納はActionを通じて行うことが出来ます。propertyへのアクセスはScopeと名前を組み合わせた"scope.name"という文字列で行うことが出来ます。

MemoryのScopeの種類は以下を参考にしています。

learn.microsoft.com

今回特に意識したのは以下のScopeです。

  • Settings
    Botアプリケーション全体の設定が保存されます。読み取り専用です。
  • Conversation
    現在の会話が継続している間、保持されます。
  • Dialog
    Dialogが起動中に保持されます。
  • Turn
    ユーザー-Bot間の1回のやり取りの間、保持されます。

今回作成したBotの構成

今回作成したBotは、1つのDialogに対し、2つのTriggerを持つシンプルな構成にしています。2つのTriggerについてまとめると以下の様になります。

  • Greeting(ConversationUpdate activity)
    会話に新しいユーザーが追加されると実行されます。会話に参加したユーザーにメッセージを送信するActionを実行します。

  • Message received(Message received activity)
    Botがユーザーから何かメッセージを受け取ると実行されます。今回のアプリケーションのメインの処理を行い、この中でAzure OpenAIへのHTTPリクエストを送信するActionも実行します。

Botの実装

ここからは具体的な実装について、説明していきます。

Bot SettingsにAzure OpenAIとStorage accountへの接続情報を追記する

Botアプリケーションの設定情報は、アプリケーションのプロジェクトフォルダ内のsettingsフォルダに格納されたappsettings.jsonに記載されています。今回のBotアプリケーションはAzure OpenAIとStorage accountへの接続を行うため、それらの接続に必要となる情報を設定ファイルに書き込んでおきます。

Bot Framework Composerからappsettings.jsonを編集することが出来ます。Bot Settingsを開き、"Advanced Settings View(json)"のスライドをオンにすると、appsettings.jsonを編集することが出来るようになります。

Azure OpenAIとStorage accountへの接続情報を、以下の様に入力しておきます。

{
  ...
  "azopenai": {
    "endpoint": "https://xxxx.openai.azure.com/openai/deployments/xxxxx",
    "token": "token"
  },
  "logstorage": {
    "connection": "DefaultEndpointsProtocol=https;AccountName=xxxx;AccountKey=xxxx;EndpointSuffix=core.windows.net",
    "table": "log"
  },
  ...
}

Storage account tableにログを書き込むためのPackageのインストール

Bot Framework Composerでは外部Packageをアプリケーションにインストールして機能追加することが出来ます。今回Azure OpenAIへのリクエストとレスポンスの履歴をStorage accountのTableに書き込むようにするため、Storage account Tableへのアクセスが容易になる以下のPackageを追加しました。

github.com

Triggerの実装

Greeting(ConversationUpdate activity)

新しいBotを作る時、Templateを選択することが出来ます。今回はC#の"Empty Bot"をTemplateとして選択しました。"Empty Bot"で作るとGreeting(ConversationUpdate activity)Triggerは自動的に生成され、その処理内容は以下の様になります。そのままでも動作しますが、"Send a response"のActionを編集して日本語の挨拶を返すように編集しました。

Greeting Triggerで行われる処理

Message received(Message received activity)

ユーザーからメッセージを受け取ると実行される処理で、このアプリケーションのメインの処理になります。こちらは実行しているActionについて、1つずつ内容を紹介します。

Message recieved Triggerの処理

Azure OpenAIのパラメータの設定

まずAzure OpenAIを利用する際のモデルのパラメータを設定するActionです。DialogをScopeとするpropertyに各パラメータをセットしています。

Azure OpenAIのモデルのパラメータの設定をします。

System Messageの設定

今回使用したモデルは"gpt-35-turbo-16k"です。このモデルにはその動作の大枠を指示するSystem Messageを与えることが出来ます。以下のActionでSystem MessageをDialogをScopeとするpropertyに格納しています。

System Messageをpropertyに格納しています。

ユーザー入力メッセージを取得する

ユーザーがBotに入力したメッセージを取得し、Azure OpenAIに入力する形式に変換してpropertyに保存します。Botに入力されたメッセージはturn.activity.textというpropertyに自動的に格納されるため、その値を参照しています。

Azure OpenAIに送る形式にデータを整形します。

会話の履歴を保持する配列への追加

ユーザーの入力とAzure OpenAIの回答は配列としてpropertyに格納していきます。

会話の履歴の配列を編集します。

配列型のpropertyを編集するためのActionがBot Framework Composerには用意されており、Actionの設定は以下の様にしています。会話の履歴は会話を通じて保持できるConversation Scopeのpropertyに持たせるようにしました。

配列編集用のActionの設定内容

Azure OpenAIへのHTTPリクエストの送信

Azure OpenAIにHTTPリクエストを送信するActionです。

HTTPリクエストを送信するAction

こちらがActionの設定内容です。

HTTPリクエストを送信するActionの設定内容

UrlとHeadersの"api-key"はappsettings.jsonに記載した値を取得するため、それぞれSettings Scopeのpropertyを参照しています。ややこしいのがリクエストボディに当たるBodyの設定で、以下の様にBot Framework Composerの組み込み関数を駆使して設定しています。

={
  "messages":flatten(createArray(dialog.system_message,conversation.chat_history)), 
  "max_tokens": dialog.max_tokens, 
  "temperature": dialog.temperature, 
  "frequency_penalty": dialog.frequency_penalty, 
  "presence_penalty": dialog.presence_penalty, 
  "top_p":dialog.top_p, 
  "stop":dialog.stop
}

"messages"の値のところが特に複雑で、まずSystem Messageを格納したdialog.system_messageと会話の履歴を格納した配列conversation.chat_historyを結合して配列の配列を作ります。そのあと組み込み関数flattenを使って一次元の配列に変換する、ということを行っています。

このActionで取得されたAzure OpenAIのレスポンスはturn.resultspropertyに格納しています。

Azure OpenAIのレスポンスをユーザーに送信する

前のActionで取得したAzure OpenAIのレスポンスをパースし、応答テキスト部分だけをユーザーに送信します。応答テキストはturn.results.content.choices[0].message.contentに格納されています。

propertyにアクセスし、取得した値をユーザーに送信する

会話の履歴を保持する配列への追加

この部分はユーザーの入力を会話の履歴conversation.chat_historyの配列に追加するのと同様です。

Azure OpenAIからのレスポンスを取得し、会話の履歴の配列に格納しています

ログの出力

ユーザーの入力と、それに対するAzure OpenAIの応答をログファイルに出力します。出力先はStorage accountのTableです。このActionは最初に追加したPackageをインストールすることで使用可能になります。

Storage account Tableへの処理を行うAction

Actionの設定は以下の様になっており、出力先のConnection StringTable Nameを指定する必要があります。これらはappsettings.jsonに記載した値を参照させています。

Tableにログを出力するAction設定内容

書き出すデータの形式はJson形式で指定するのですが、こちらもBot Framework Composerの組み込み関数を駆使して指定しました。やっていることは、動的にテキストの内容が変わる部分を文字列結合の組み込み関数concatで繋げ、最後にjson関数でJsonに変換する、というかなり力技な内容です。

=json(
  concat(
    '{"partitionKey":"teams-gpt", "RowKey":"',string(turn.activity.timestamp),'","UserName":"',turn.activity.from.name,'", "HumanInput":"',dialog.last_input.content,'","AIOutput":"',dialog.last_output.content,'"}'))
過去の会話の履歴を消す

最後はこれまでの会話の履歴の長さをチェックし、長さが10を超えた場合は一番古いユーザーの入力とAzure OpenAIの応答を配列conversation.chat_historyから消す、という処理です。これによりAzure OpenAIに入力するテキストが長くなりすぎない様に制御します。過去の履歴を消すために、配列の先頭から要素を消す組み込み関数skipを使用しました。

条件分岐を使用します

以上がMessage received(Message received activity)Triggerで実行される処理です。

Botのテスト

Bot Framework ComposerでBotのテストをすることが出来ます。Botを起動し、Bot Framework ComposerのOpen Web Chatを開くと、以下の様にBotを試すことが出来ます。

BotをWeb Chatでテストしている様子

ちゃんと応答してくれています!

まとめ

ということで、今回はBot Framework Composerを使ったAzure OpenAIと接続したBotアプリケーションの作り方について、これまで試してきたことをご紹介しました。Botアプリケーションは1から作ろうとすると考えなければならないことが多々ありますが、Bot Framework Composerを使えばそれらの大部分を気にせずBotアプリケーションの開発に取り掛かることが出来る手軽さがあります。一方で細かい調整を行おうとすると少し苦労するところもあったので、もう少し慣れが必要だな、と思いました。

今回はDialogが1つだけのシンプルな構成のBotアプリケーションでしたが、複数のDialogを使った複雑なアプリケーションの開発も今後試してみたいと思います。