Copilot Studio から Azure OpenAI Services に会話履歴を連携する
はじめに
前回の記事に引き続き、Microsoft Teams から Copilot Studio で Azure OpenAI Services (AOAI) を呼び出すにあたり、会話履歴を連携させるようにしてみます。
記事は 2024 年 3 月 31 日時点の内容を元に執筆しています。常に最新の情報を確認するようにしてください。
前回の記事で実装した仕組み上の問題点
AOAI は会話履歴を保持していません。AOAI はクライアント (この場合、Copilot Studio) から送られてきたメッセージの内容だけを見て応答しています。
このため、クライアントからのメッセージに会話履歴が含まれていない場合には、「それについて、もう少し詳しく教えて。」「いまの回答とは別のアイデアはないですか?」といった直前のやりとりを加味した会話が成立しない状態になります。
具体的には以下のようなやりとりができません。
今回の記事のゴールは、このような会話のキャッチボールができるようになることです。(これは、Copilot Studio の「テスト」機能から行ったものですが、毎回 AOAI から同じ応答が返されるとは限りません。)
対応策
クライアントから AOAI に会話履歴を含めてリクエストを発行するようにリクエストパラメータを変更します。この実装にあたっては、会話履歴の持ち方についての検討が必要になります。
一般的な会話履歴の管理方法には、以下のような方法が考えられます。(本記事では A の方法を紹介します。)
A. Copilot Studio のグローバル変数で会話履歴を管理する
Copilot Studio のチャット処理で定義できる変数には以下の 3つがあります。
- トピック変数
特定のトピック内でのみ生存する変数です。例えば、ユーザーが何かチャットから何かを入力し、チャットをトリガーとして起動した処理が完了するまでの間で生存する変数です。トピック変数は生存期間が限定されるため、2つ目のチャットメッセージにまで変数の内容を持ち越すことができません。 - グローバル変数
複数のトピックを横断して使用できる変数です。会話のリセットなどの特定のイベントによって初期することができます。 - システム変数
ボットで自動的に作成されるものです。例えばチャットで入力されたテキストメッセージなどがあります。
グローバル変数に入力されたチャットメッセージや AOAI からの応答メッセージを保持させることで、会話履歴を AOAI に渡すことができます。
B. 会話履歴をデータベースなどで管理する
Copilot Studio の会話履歴は、Dataverse の ConversationTranscript テーブルに格納されています。この仕組みを利用して Copilot Studio から Power Automate を呼び出し、会話履歴を Dataverse の ConversationTranscript テーブルから取得する、といった方法が考えられます。 もちろん、CosmosDB などを使用して独自に会話履歴を管理することも可能です。
2024/04/01 追記
Dataverse の ConversationTranscript テーブルにはリアルタイムに会話データが確認されないことを確認しました。このため、リアルタイムに Copilot Studio から AOAI に会話履歴を連携させようとすると何らかの独自実装が必要になります。
詳細は、Copilot Studio の会話履歴が保存される ConversationTranscript テーブルを探る を参照ください。
グローバル変数で会話履歴を保持する実装例
それでは、グローバル変数に会話履歴を保持する方法を実装してみます。
前回の記事では、何らかのメッセージを受けると、受け取ったメッセージを REST API で AOAI に飛ばして、AOAI から得られた応答をチャットの戻す、という実装を行いました。会話履歴を AOAI に連携させるには、REST API で AOAI に渡すメッセージに会話履歴を含める必要があります。
- REST API で AOAI にメッセージを渡す前に過去の会話履歴を取得
- REST API で AOAI に渡すメッセージに会話履歴を含める
- REST API で AOAI から得られた応答を会話履歴を保存
前回の記事で作成したフローの以下の部分で会話履歴に関わる処理の追加が必要になります。現時点での Copilot Studio では細かいデータの加工を行うことができないため、Power Automate のフローを使用します。
やっていることは単純で AOAI に連携させるデータに会話履歴を追加するだけです。
前回の実装では system role と user role が 1 つずつで、 user role (今回の質問) で AOAI への質問を記述しているのみでした。AOAI へ会話履歴を連携するには system role と今回の質問を記述している user role の間に過去の会話のやり取りを追加します。
過去に AOAI に問い合わせた質問を user role として定義し、AOAI からの回答を assistant role で定義します。やり取りが増えた会話履歴は、3、4、5.. というように下に追加していきます。
{
"messages": [
{
"role": "system",
"content": "You are an AI assistant that helps people find information."
},
{
"role": "user",
"content": "(会話履歴) AOAI への質問1"
},
{
"role": "assistant",
"content": "(会話履歴) AOAI からの回答1"
},
{
"role": "user",
"content": "(会話履歴) AOAI への質問2"
},
{
"role": "assistant",
"content": "(会話履歴) AOAI からの回答2"
}
{
"role": "user",
"content": "今回の質問"
}
],
"max_tokens": 800,
"temperature": 0.7,
"frequency_penalty": 0,
"presence_penalty": 0,
"top_p": 0.95,
"stop": null
}
1.グローバル変数の定義と初期化
Copilot Studio のグローバル変数に会話履歴を保持させるにあたり、グローバル変数を定義します。
トピック (システム) から「会話の開始」を有効化し、グローバル変数として会話履歴 (ここでは、"conversation_history" とします。) を保持する変数を定義します。
本記事では、チャットボットとやりとりした会話履歴をここで定義したグローバル変数に格納していきますが、何らかのタイミングで初期化しないと不都合が生じます。ここでは、会話の終了時にグローバル変数を初期化させることにします。グローバル変数の初期化のため、「トピック」の「最初からやり直す」を有効化します。
また、「最初からやり直す」から呼び出される「トピック (システム)」の「会話をリセットする」も有効化します。
「会話をリセットする」フローの中に規定で変数の初期化が組み込まれています。これで会話の終了時にグローバル変数が初期化されるようになります。
以下はここで使用する会話履歴のサンプルです。このフォーマットで会話履歴を管理します。サンプルにあるように "history" 配列に「質問」と「回答」のセットを格納していきます。
- 「"role": "user"」: ユーザーが AOAI にチャットした質問
- 「"role": "assistant"」: AOAI が応答したメッセージ
{
"history": [
{
"role": "user",
"content": "AOAI への質問1"
},
{
"role": "assistant",
"content": "AOAI からの回答1"
},
{
"role": "user",
"content": "AOAI への質問2"
},
{
"role": "assistant",
"content": "AOAI からの回答2"
}
]
}
2.会話履歴保存処理の作成
まずは会話履歴を保存する処理を作成します。前回の記事で作成したフローの「値の解析」と最後にチャットに応答する「Message」の間にノードを追加します。
「アクションを呼び出す」-「フローの作成」クリックし、Power Automate のフローを作成します。
会話履歴の保存処理 (Power Automate のフロー)
少し長いのですが、全体象はこのようになりました。(もっと簡単に作る方法はあると思います。)
処理の概要:
Copilot Studio から、Power Automate に以下のデータを受け取ります。
- Copilot Studio のグローバル変数に保持されている会話履歴
- Copilot Studio から行われた直近の AOAI へのメッセージ (質問)
- 直近の AOAI から受け取ったメッセージ (質問) に対する応答
Power Automate で Copilot Studio から受け取った「会話履歴」に「AOAI へのメッセージ (質問) 」と「AOAI からのメッセージ (質問) に対する応答」を加えた新しい会話履歴データを生成し、Copilot Studio に返します。
以下は実装の参考になるように詳細を記述しています。
① 入力パラメータの設定
Copilot Studio から受け取るパラメータを定義します。
いずれも「テキスト」型です。
- HistoryDataString
- QuestionString
- AnswerString
② 処理変数の初期化
内部処理のため、以下の変数を定義します。
Value はすべて空にします。
- NewHistoryString (Type:String)
- tmpHistoryDataJson (Type:Object)
- tmpHistoryDataArray (Type:Array)
- NewQuestionJson (Type:Array)
- NewAnswerJson (Type:Array)
③ 直前の会話履歴データの作成
Copilot Studio からの受け取った会話履歴 (HistoryDataString) に中身があるかどうかで条件分岐させます。
入力パラメータの「動的コンテンツ」-「HistoryDataString」を指定すると以下のように変換されます。実行環境に依存する (Copilot Studio から受け取るパラメータの順番定義) ので直接コードを記述するのではなく、Length() 関数の中の変数は動的コンテンツから指定します。
③-1. 会話履歴がある場合
Copilot Studio から受け取った会話履歴 (HistoryDataString) をテキスト型から JSON 型に Parse します。
データ定義にはこちらを使用します。
※「サンプルのペイロードを使用してスキーマを作成する」からサンプルの会話履歴データを読み込ませてください。
{
"type": "object",
"properties": {
"history": {
"type": "array",
"items": {
"type": "object",
"properties": {
"role": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"role",
"content"
]
}
}
}
}
次に、先ほど Json に変換した本文データをそのまま「tmpHistoryDataJson」変数に格納します。
最後に Json の history 配列データを「tmpHistoryArray」配列に格納します。
③-2. 会話履歴がない場合
tmpHistoryDataJson 変数を以下のデータ (history 配列を空) で初期化します。
④ 直近の AOAI へのメッセージ (質問) から Json データ作成
新しい会話履歴データに格納する直近の質問データを「NewQuestionJson」変数を更新します。
ここで以下のようなデータを作成しています。
{
"role": "user",
"content": "直近の AOAI へのメッセージ (質問) "
}
⑤ 直近の AOAI から受け取ったメッセージ (質問) に対する応答から Json データ作成
新しい会話履歴データに格納する直近の質問に対する回答データを「NewAnswerJson」変数を更新します。
ここで以下のようなデータを作成しています。
{
"role": "assistant",
"content": "直近の AOAI から受け取ったメッセージ (質問) に対する応答"
}
⑥ 新しい会話履歴データの作成
Copilot Studio に戻す会話履歴データを作成します。
「NewHistoryData」変数に「現行の会話履歴」「直近の質問」「直近の質問に対する回答」をマージします。
この処理ではここまでで作成したデータをマージしています。
{
"history": [
{
// (Copilot Studio から受け取った) 直近までの会話履歴データ
},
{
// ④ で作成したデータ
},
{
// ⑤ で作成したデータ
}
]
}
⑦ 新しい会話履歴データの応答
先で作成した「NewHistoryData」変数を Copilot Studio に戻します。
作成した Power Automate の処理は適当な名前を付けて保存します。
3.会話履歴保存処理の呼び出し
Copilot Studio に戻ってきます。Copilot Studio 側で、先ほど作成した Power Automate に渡すパラメータを指定します。
Input:
- HistoryDataString: Global.conversation_history (グローバル変数の会話履歴)
- QuestionString: Activbity.Text (チャットメッセージ)
- AnswerString: Output (Azure OpenAI Serices の応答を Parse した応答メッセージ)
Output:
- historydatastring: Global.conversation_history (グローバル変数の会話履歴)
これで会話履歴が、グローバル変数に格納されるようになりました。
4.会話履歴の取得 (AOAI メッセージの作成)
今度は AOAI を呼び出す前に会話履歴の取得処理を追加します。こちらも先ほどの会話履歴の保存と同様に Power Automate で処理作成します。
会話履歴の取得処理 (Power Automate のフロー)
こちらの全体象です。
処理の概要
Copilot Studio から、以下のデータを受け取ります。
- Copilot Studio のグローバル変数に保持されている会話履歴
- AOAI のシステムメッセージ
- Copilot Studio から行われた直近の AOAI へのメッセージ (質問)
上記 3 つのデータから AOAI に渡す会話履歴を含むメッセージを生成し、Copilot Studio に返します。
以下は実装の詳細です。
① 入力パラメータの設定
Copilot Studio から受け取るパラメータを定義します。
いずれも「テキスト」型です。
- HistoryDataString
- SystemMessageString
- QuestionString
② 処理変数の初期化
内部処理のため、変数を定義します。
以下の変数の Value はすべて空にします。
- NewSystemMessageJson (Type:Array)
- NewQuestionJson (Type:Array)
- NewMessage (Type:String)
tmpMessage は以下で定義します。 - tmpMessage (Type:Object)
次に入力パラメータから変数を更新します。
- AOAI のシステムメッセージから「NewSystemMessageJson」変数を更新します。
ここでは以下のようなデータを作成して変数を更新しています。
{
"role": "system",
"content": "AOAI のシステムメッセージ"
}
- AOAI へのメッセージ (質問) から「NewQuestionJson」変数を更新します。
ここでは以下のようなデータを作成して変数を更新しています。
{
"role": "user",
"content": "AOAI へのメッセージ (質問)"
}
③ 直近の会話履歴データの作成
Copilot Studio からの受け取った会話履歴 (HistoryDataString) に中身があるかどうかで条件分岐させます。
③-1. 会話履歴がある場合
Copilot Studio から受け取った会話履歴 (HistoryDataString) をテキスト型から JSON 型に Parse します。
データ定義にはこちらを使用します。
※こちらも「サンプルのペイロードを使用してスキーマを作成する」からサンプルの会話履歴データを読み込ませてください。
{
"type": "object",
"properties": {
"history": {
"type": "array",
"items": {
"type": "object",
"properties": {
"role": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"role",
"content"
]
}
}
}
}
次に、先ほど Parse した Json の history データから role と content を抽出します。
Copilot Studio で保持している会話履歴データはこうなっています。
{
"history": [
{
"role": "user",
"content": "..."
},
{
"role": "assistant",
"content": "..."
},
...
]
}
ここでは以下のように history の配列データのみ抜き出しています。
[
{
"role": "user",
"content": "..."
},
{
"role": "assistant",
"content": "..."
},
...
]
最後に AOAI に渡す新しいメッセージを作成します。
こういうメッセージを作成しています。
{
"messages": [
{
// ② で作成した AOAI のシステムメッセージ
},
{
// ③-1. で取得した直近までの会話履歴データ
},
{
// ② で作成した AOAI へのメッセージ (質問)
}
]
}
③-2. 会話履歴がない場合
以下のように新しいメッセージを作成します。
会話履歴がないので、こういうデータになります。
{
"messages": [
{
// ② で作成した AOAI のシステムメッセージ
},
{
// ② で作成した AOAI へのメッセージ (質問)
}
]
}
④ AOAI に渡すメッセージデータの応答
先で作成した「NewMessage」変数を Copilot Studio に戻します。
ここまでで作成した Power Automate の処理は適当な名前を付けて保存します。
5.会話履歴の取得 (AOAI メッセージの作成) 処理の呼び出し
ここからはまた Copilot Studio での操作になります。Copilot Studio 側で Power Automate に渡すパラメータを指定します。
Input:
- HistoryDataString: Global.conversation_history (グローバル変数の会話履歴)
- SystemMessageString: "You are an AI assistant that helps people find information." (ここでは、AOAI の規定のシステムメッセージを定義)
- QuestionString: Activbity.Text (チャットメッセージ)
Output:
- aoaistringmessage: 他の用途で使用していない任意の string 変数を指定します。
これで過去の会話履歴を含めた AOAI へのメッセージが作成できました。
6.AOAI に渡すメッセージパラメータの追加
5.までの処理で AOAI に渡すメッセージデータが作成されましたが、REST API で AOAI に渡すためには、他のパラメータとマージする必要があります。
まずは、5.の処理で Power Automate からテキスト型で応答が返されるため、Json 形式に Parse します。Parse したデータを "ParseMessages" に格納します。
次に AOAI の REST API の Body に渡すための変数を新規 ("InputMessage" とします。) に作成し、先で取得した AOAI のメッセージを埋め込みます。「messags: Topic.ParseMessages.messages」が先で作成された AOAI のメッセージでータです。
{
messages:Topic.ParseMessages.messages,
max_tokens: 800,
temperature: 0.7,
frequency_penalty: 0,
presence_penalty: 0,
top_p: 0.95,
stop: Blank()
}
最後に AOAI の REST API の本文に先ほど作成した変数 (Topic.InputMessage) を渡します。
まとめ
Copilot Studio から AOAI に会話履歴を連携させる方法の一つとしてグローバル変数を使用する方法を確認しました。会話がリセットされるまで直前の会話も含めて AOAI に連携されます。
Copilot Studio の会話履歴が保存される ConversationTranscript テーブルを探る で、Copilot Studio の会話履歴が規定で保存される ConversationTranscript テーブルを確認しましたが、現状で Copilot Studio から AOAI に会話履歴を連携させようとすると何らかの独自実装が必要になりそうです。
Discussion