ささみ学習帳 - sasami's study book

ささみ学習帳

Microsoft365 や Power Platform について学んだこと・アイデアのメモ

Azure OpenAI Serviceを利用してTeamsチャネルにダミー会話のスレッドを作成する Power Automate クラウドフロー💎

 

Teamsのチャネルに複数人で会話をしてる風のダミースレッドを作成します。

 

このフローで実現できること

  • Teamsのチャネルに最大4人での会話風ダミーメッセージのスレッドを作成します
  • Azure OpenAI Serviceを使うことである程度意味のある会話のスレッドにします
    • 指定したキーワードに関する会話を生成
    • スレッド内のメッセージ数を設定可能
    • 会話の状況を指定可能「例)真剣な会議中」

 

実行イメージ

 

・gpt-3.5-turbo

・gpt-4

 

必要なもの

  • Microsoft Teamsが利用可能なアカウント
    • 職場または学校アカウントです
    • 複数人の会話を実現するため4つのアカウントを切り替えてメッセージ投稿を行います
  • Azure OpenAI Serviceが利用可能であること
    • 今回はgpt 3.5 turbo 16kを使っています
    • ごくごく初歩的な使い方なのでOpenAI APIでもほぼ同じ事が実装可能です。
  • Power Automate プレミアムコネクタが利用可能であること
    • 有償ライセンスもしくは検証用途であればPowerApps開発者プラン
    • HTTPコネクタを利用するため必要になります

 

準備する

1.Azure  OpenAI Serviceをデプロイし利用可能としておく

こちらの記事で準備したものを使っています。

 

2.Microsoft Teams コネクタに4つのアカウントの接続を作成する

フロー実行アカウントに、他の3アカウントのTeamsコネクタの接続を作成します。

Power Automateの画面右のメニューの接続から作成するか、またはフローエディタでTeams コネクタのアクションの「…」→「新しい接続の追加」で追加できます。

 

フロー全体図

※スイッチアクションを閉じた状態です。展開するとこうなります。

 

フロー解説

1.【トリガー】Teamsコネクタ-作成ボックスから

このトリガーを使うことで、Teamsのメッセージ作成ボックスに追加したWorkflowアプリからこのフローが起動できます。

下記のようなアダプティブカードを設定し、Azure OpenAI (以降AOAI)に渡すパラメータを設定できるようにしています。

{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.3",
    "body": [
        {
            "type": "TextBlock",
            "text": "ダミースレッドを作成",
            "weight": "Bolder",
            "size": "Medium"
        },
        {
            "type": "TextBlock",
            "text": "指定したキーワードに関して雑談するダミースレッドを作成します。",
            "isSubtle": true,
            "wrap": true
        },
        {
            "type": "Input.Text",
            "spacing": "None",
            "id": "keyword",
            "label": "キーワード",
            "isRequired": true,
            "errorMessage": "キーワードは必須です"
        },
        {
            "type": "Input.Number",
            "id": "maxMessages",
            "value": 15,
            "max": 30,
            "min": 1,
            "label": "スレッドの会話数",
            "isRequired": true,
            "errorMessage": "会話数は必須です"
        },
        {
            "type": "Input.Text",
            "placeholder": "Placeholder text",
            "id": "situation",
            "label": "状況設定",
            "value": "業務会議中の真剣な会話",
            "isRequired": true,
            "errorMessage": "状況設定は必須です"
        }
    ],
    "actions": [
        {
            "type": "Action.Submit",
            "title": "作成"
        }
    ]
}

 

2.コントロール-条件-チャットからの呼び出しの場合は終了する

 

作成ボックスからトリガーを使用した場合、チャネルからだけではなくチャットからもフローの呼び出しが可能となります。しかし、今回のフローはチャネルからの呼び出しのみを想定していますので、チャットからの呼び出しの場合はメッセージ表示後にフローを終了させています。

empty関数でチームIDの有無を判定し、存在しない場合はチャットからの呼び出しだと判断できます。

empty(triggerBody()?['teamsFlowRunContext']?['channelData']?['team']?['aadGroupId'])

 

2-1.[はいの場合]タスクモジュールで応答

チャネルからの呼び出しのみ対応というメッセージを表示します。

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "ダミースレッドを作成する"
        },
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "text": "このフローはチャネルからの呼び出しのみ対応しています。",
                    "wrap": true,
                    "id": "answer",
                    "height": "stretch",
                    "separator": true
                }
            ],
            "separator": true,
            "minHeight": "250px",
            "verticalContentAlignment": "Top",
            "height": "stretch"
        }
    ],
    "msteams": {
        "width": "Full"
    },
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.4"
}

 

2-2.[はいの場合]終了

フローを終了します。

  • 状態
    • 取り消し済み

 

2-3.[いいえの場合]

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "ダミースレッドを作成する"
        },
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "text": "このメッセージを閉じて、しばらくお待ちください。",
                    "wrap": true,
                    "id": "answer",
                    "height": "stretch",
                    "separator": true
                }
            ],
            "separator": true,
            "minHeight": "250px",
            "verticalContentAlignment": "Top",
            "height": "stretch"
        }
    ],
    "msteams": {
        "width": "Full"
    },
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.4"
}

チャネルからの呼び出しの場合は次のようなメッセージを表示します。

AOAIの応答時間次第でタイムアウトが発生してしまうため、苦肉の策でこのタイミングでメッセージを返しています。

 

3.コントロール-スコープ-トリガーの出力を参照

トリガーの出力の値で使用するものを作成アクションで出力しています。

これはトリガーの出力を後続のアクションで直接使用していると、トリガーを変更した場合に修正箇所が多くなってしまう為、一旦作成アクションを経由することで変更箇所を少なくしています。トリガーを変更する可能性がない場合はこのようなステップは不要です。

  • teamId
    • フローを実行したチームIDです。このトリガーの標準で返す値の1つです。
  • channelId
    • フローを実行したチャネルのIDです。このトリガーの標準で返す値の1つです。
  • keyword
    • トリガーのアダプティブカードで定義した「キーワード」の値です。
    • このキーワードに関する会話をAOAIで生成します。
  • maxMessages
    • トリガーのアダプティブカードで定義した「スレッドの会話数」の値です。
    • スレッド内に投稿する会話の数です。実際にはこの数から多少増減があります。
  • situation
    • トリガーのアダプティブカードで定義した「状況設定」の値です。
    • キーワードと別に会話のシチュエーションを調整します。

 

4.変数を初期化する-messageId

スレッドの親メッセージのメッセージIDを格納するための変数です。

 

5.スコープ-AOAI Completion

5-1.パラメータの準備

AOAI completion に渡すパラメータを定義しています。

 

5-1-1.作成-APIKey
あらかじめ準備しておいたAOAIのAPIキーを設定します。

 

5-1-2.作成-system content

あなたは演劇の脚本家です。
Please follow the following rules to answer the questions.
---
- 会話のみで脚本を構成してください
- 会話は@{outputs('作成-maxMessages')}最大個で終了する脚本にしてください
-  "は使用しないでください
- 脚本の状況設定は「@{outputs('作成-situation')}」とする。
-  登場人物は最大でA,B,C,Dの4人までとします
- A,B,C,Dの人物像・性別・性格はそれぞれ個性的に設定してください
- A,B,C,Dの人物の口調は脚本の状況設定に合わせて設定する。
- 脚本は必ずAの発言から始めてください
- 【重要】生成する脚本は次の例のようなJSON形式を厳守して出力してください。
from:発言した人物
text:日本語の会話文
---
(例)
[
  { "from": "A", "text":"あいうえお。"},
  { "from": "B", "text":"あいうえお。"},
  { "from": "C", "text":"あいうえお。"},
  { "from": "D", "text":"あいうえお。"}
]

 

5-1-3.作成-user content

「@{outputs('作成-keyword')}」に関して会話している脚本を作ってください

 

5-2.HTTP-AOAI Completion

APIを呼び出します。

  • 方法
    • POST
  • URI
    • あらかじめAOAI Sdutioで取得したエンドポイントURI
  • ヘッダー
    • Content-type:application/json
    • api-key:@{outputs('作成-API_Key')}
  • クエリ
    • 未設定
  • 本文
    • 下記
  • Cookie
    • 未設定
  • 認証
    • なし

[HTTPアクションの本文]

{
  "messages": [
    {
      "role": "system",
      "content": "@{outputs('作成-system_content')}"
    },
    {
      "role": "user",
      "content": "@{outputs('作成-user_content')}"
    }
  ],
  "temperature": 0.3,
  "top_p": 0.95,
  "frequency_penalty": 0,
  "presence_penalty": 0,
  "max_tokens": 4096,
  "stop": null
}

 

5-3.JSONの解析

HTTPアクションの出力JSONをパースします。

  • コンテンツ
    • @{body('HTTP-AOAI_completion')}
  • スキーマ
    • 下記
{
    "type": "object",
    "properties": {
        "id": {
            "type": "string"
        },
        "object": {
            "type": "string"
        },
        "created": {
            "type": "integer"
        },
        "model": {
            "type": "string"
        },
        "choices": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "index": {
                        "type": "integer"
                    },
                    "finish_reason": {
                        "type": "string"
                    },
                    "message": {
                        "type": "object",
                        "properties": {
                            "role": {
                                "type": "string"
                            },
                            "content": {
                                "type": "string"
                            }
                        }
                    }
                },
                "required": [
                    "index",
                    "finish_reason",
                    "message"
                ]
            }
        },
        "usage": {
            "type": "object",
            "properties": {
                "completion_tokens": {
                    "type": "integer"
                },
                "prompt_tokens": {
                    "type": "integer"
                },
                "total_tokens": {
                    "type": "integer"
                }
            }
        }
    }
}

 

5-4.JSONの解析-content

AOAIにはJSON形式で回答するよう指示していますので、下記のようなJSON形式で生成したメッセージを返してきます。

[
  { "from": "A", "text":"あいうえお。"},
  { "from": "B", "text":"あいうえお。"},
  { "from": "C", "text":"あいうえお。"},
  { "from": "D", "text":"あいうえお。"}
]

このJSONをパースします。

first(body('JSON_の解析')?['choices'])?['message']?['content']
{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "from": {
                "type": "string"
            },
            "text": {
                "type": "string"
            }
        },
        "required": [
            "from",
            "text"
        ]
    }
}

 

6.「スコープ-AOAI completion」のエラー処理

AOAIの呼び出しで想定外のエラーが発生した場合のエラー処理です。

6-1.Teamsコネクタ-チャットまたはメッセージで投稿する2

実行条件の構成で、「スコープ-AOAI completion」でエラーが発生 or タイムアウトが発生した場合に動作するよう設定します。

  • 投稿者
    • フローボット
  • 投稿先
    • Chat with Flow bot
  • Recipient
    • @{triggerBody()?['teamsFlowRunContext']?['from']?['aadObjectId']}
    • トリガーの操作元ユーザーID
  • Message
    • Teamsチャネルにダミースレッドを作成するフローでエラーが発生しました。\n\nAzure OpenAI Serviceの呼び出しで何らかのエラーが発生しました。\n詳細はフローの実行結果を確認してください。
  • IsAlert
    • はい
6-2.終了3

フローを終了します。

 

7.スコープ-Teamsチャネルにスレッドを作成

7-1.作成-親メッセージ

アレイから先頭のメッセージを取り出します。このメッセージでスレッドを開始するため先行のメッセージだけ使用するアクションが異なります。

first(body('JSON_の解析-content'))

ここではfirst関数でアレイの1つ目の要素を取り出しています。

 

7-2.Teamsコネクタ-チャットまたはチャネルでメッセージを投稿する

7-1.で取り出した先頭メッセージで、新規スレッドとしてメッセージを投稿します。

  • 投稿者
    • ユーザー
  • 投稿先
    • チャネル
  • Team
    • @{outputs('作成-teamId')}
  • Channel
    • @{outputs('作成-channelId')}
  • Message
    • @{outputs('作成-親メッセージ')?['text']}
  • ※使用する接続
    • アカウントA

 

7-3.変数の設定-messageId

7-2.で投稿したメッセージのmessageIdを変数に格納します。

後続のメッセージは7-2.の返信メッセージとして投稿するため必要になります。

  • 名前
    • messageId
    • @{outputs('チャットまたはチャネルでメッセージを投稿する')?['body/id']}

 

7-4.作成-返信メッセージ

skip関数で2番目以降のメッセージを抽出します。これらを返信メッセージとして投稿します。

skip(body('JSON_の解析-content'),1)

 

7-5. Apply to each-返信メッセージ1つづつ

  • 以前の手順から出力を選択
    • @{outputs('作成-返信メッセージ')}

 

7-5-1. スイッチ-メッセージを投稿

返信メッセージのFromの値で切り替えます。

  • オン
    • @{items('Apply_to_each-返信メッセージ1つづつ')?['from']}
  • ケース1:次の値と等しい
    • A
  • ケース2:次の値と等しい
    • B
  • ケース3:次の値と等しい
    • C
  • ケース4:次の値と等しい
    • D

スイッチアクションのケース1〜4どれも応答アクションの設定は同じですが、使用する接続をそれぞれA〜Dに振り分けています。

 

7-5-2. チャネル内のメッセージで応答します(A〜D)

7-.2で投稿したメッセージへの返信としてメッセージを投稿します。

  • 投稿者
    • User
  • 投稿先
    • Channel
  • Message ID
    • @{variables('messageId')}
  • Team
    • @{outputs('作成-teamId')}
  • Channel
    • @{outputs('作成-channelId')}
  • Message
    • @{items('Apply_to_each-返信メッセージ1つづつ')?['text']}
  • ※使用する接続
    • アカウントA~D

 

7-5-3. チャネル内のメッセージで応答します(A〜D)

スイッチの既定ケースは、Flow botとして返信メッセージを投稿します。通常は使用しないはずですが、万が一AOAIの出力がA〜D以外の設定を作り出した場合を想定して配置します。

  • 投稿者
  • 投稿先
    • Channel
  • Message ID
    • @{variables('messageId')}
  • Team
    • @{outputs('作成-teamId')}
  • Channel
    • @{outputs('作成-channelId')}
  • Message
    • @{items('Apply_to_each-返信メッセージ1つづつ')?['text']}
  • ※使用する接続
    • アカウントA

 

7-6.スケジュール-待ち時間

返信メッセージの投稿に5秒の間隔をあけます。

待ち時間なしで多数の連続投稿を行うと、Power Automateの制限に抵触する可能性がある為です。

  • カウント
    • 5
  • 単位

 

7-7.チャネル内のメッセージで応答します-会話終了message

フローによる投稿が完了した事をわかりやすくする為に最後に提携メッセージを投稿します。

  • 投稿者
  • 投稿先
    • Channel
  • Message ID
    • @{variables('messageId')}
  • Team
    • @{outputs('作成-teamId')}
  • Channel
    • @{outputs('作成-channelId')}
  • Message
    • (会話はここで終わっている……)
  • ※使用する接続
    • アカウントA

 

改善したいこと

  • 会話内容は結構適当
    • gpt-3.5-turbo でざっくりプロンプトで処理している為かなかなかおかしな会話が生成される事があります。ダミーデータ目的では問題ありませんが、会話内容の精度を求めるにはプロンプトチューニングやGPT-4(課金が…)で改善の余地はありそうです。

さいごに

会話風に作成するために複数のアカウントを使用しますので、実用度は微妙なところです。例えば、Teamsの初心者向け勉強会を開催する際にこんなふうに会話できるよーっと例示するひとネタに使えるかもしれません。