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

ささみ学習帳

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

Teams チャネルのスレッドの内容を3行程度に要約する Power Automate クラウドフロー V3 チャット対応版(2/2) AOAI準備~フロー解説編💎

(2023/9/14更新)AOAIのプロンプト・パラメータを少し調整しました。

>4-8-1.パラメータの準備
・文字数の制約を削除しました
・箇条書きの数の制限を削除しました

>4-8-2.AOAI API call
・"max_tokens":4096 に増やしました

 

 

Teams チャネルのスレッドの内容を3行程度に要約する Power Automate クラウドフロー V3 チャット対応版(1/2)概要〜カスタムコネクタ作成編💎」の続きです。

 

 

 

2.Azure OpenAI の準備

2-1.Azure OpenAI 利用申請(2023年8月時点では必要です)

2-2.Azure OpenAI リソースの作成

Microsoft MVPのようさんのリソース作成の手順を参考に作成しています。

Azure OpenAI Serviceを使用する為の準備 ~リソースの作成編~ - 業務ハックLab -とある情シスの備忘録

2-3.モデルのデプロイ

こちらもMicrosoft MVPのようさんの手順を参考に作成しています。

Azure OpenAI Serviceを使用する為の準備 ~モデルのデプロイ編~ - 業務ハックLab -とある情シスの備忘録-

※チャット・チャネルで長文が投稿されることも想定し、"gpt-35-turbo-16k" で作成しました。

2-4.必要な情報を取得

Azure OpenAI Studio→プレイグラウンド→チャット

 

2-3で作成したデプロイを選択

適当なユーザーメッセージを入力しコードを表示

 

コードを表示→サンプルコードjsonを表示

エンドポイントをコピーしておく

キーをコピーしておく

jsonのサンプルコードをもとにPower Automateから実行します。

 

3.フロー全体図

長くなったのでスコープ部分は切り出しました。

スコープ-チャットの50件までのメッセージを取得

スコープ-スレッドのメッセージを取得

スコープ-AOAI completion


4.フロー解説

4-1.トリガー Teamsコネクタ-選択したメッセージの場合(V2)

チャネル内メッセージの「…」から呼び出す想定です。

 

4-2.変数を初期化する-改行

改行のみの変数を定義しています。フローの後半で使用します。

 

4-3.変数を初期化する-message

取得したメッセージを一時的に格納する変数です。


4-4.呼び出し元がチャットかチャネルか?

チャットとチャネルで処理が異なるため、フローを実行したメッセージがチャットかチャネルかで処理を分岐します。

トリガーの出力「teamId」の値の有無で呼び出し元がチャットかチャネルかを判断します。

[条件の式]

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

 

4-5.[はいの場合]スコープ-チャットの50件までのメッセージを取得

4-5-1.Get Chat Message

作成したカスタムフローのアクションでチャットのメッセージを取得します。

APIの仕様で最大50件まで取得できます。

"value"のアレイにメッセージが格納された形で出力されます。

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('448060bd-fe71-424f-b70b-3456084c7ffd')/chats('19%3Ac50468d366d94697be02aacc255033dc%40thread.v2')/messages",
  "@odata.count": 12,
  "value": [
    {
      "id": "1692184850501",
      "replyToId": null,
      "etag": "1692184850501",
      "messageType": "message",
      "createdDateTime": "2023-08-16T11:20:50.501Z",
      "lastModifiedDateTime": "2023-08-16T11:20:50.501Z",
      "lastEditedDateTime": null,
      "deletedDateTime": null,
      "subject": null,
      "summary": null,
      "chatId": "19:c50468d366d94697be02aacc255033dc@thread.v2",
      "importance": "normal",
      "locale": "en-us",
      "webUrl": null,
      "channelIdentity": null,
      "policyViolation": null,
      "eventDetail": null,
      "from": {
        "application": null,
        "device": null,
        "user": {
          "@odata.type": "#microsoft.graph.teamworkUserIdentity",
          "id": "5e41bcfc-97b9-40ec-9f2c-8bef94b140d7",
          "displayName": "ジャイアント パンダ",
          "userIdentityType": "aadUser",
          "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        }
      },
      "body": {
        "contentType": "html",
        "content": "<p>こちらこそ、ありがとうございます。皆さんと一緒にうまい棒を楽しめて幸せでした。これからもうまい棒を食べ続けましょう。</p>"
      },
      "attachments": [],
      "mentions": [],
      "reactions": []
    },
    {
      "id": "1692184842691",
      "replyToId": null,
      "=======以下省略========="
    }
  ]
}

 

4-5-2.JSONの解析-chat

Get Chat Messagesアクションで取得した結果をパースします。

メッセージのアレイとなっている['body/value']の部分のみを解析しています。

[スキーマ]

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "id": {
                "type": "string"
            },
            "replyToId": {},
            "etag": {
                "type": "string"
            },
            "messageType": {
                "type": "string"
            },
            "createdDateTime": {
                "type": "string"
            },
            "lastModifiedDateTime": {
                "type": "string"
            },
            "lastEditedDateTime": {},
            "deletedDateTime": {},
            "subject": {},
            "summary": {},
            "chatId": {
                "type": "string"
            },
            "importance": {
                "type": "string"
            },
            "locale": {
                "type": "string"
            },
            "webUrl": {},
            "channelIdentity": {},
            "policyViolation": {},
            "eventDetail": {},
            "from": {
                "type": [
                    "object",
                    "null"
                ],
                "properties": {
                    "application": {},
                    "device": {},
                    "user": {
                        "type": "object",
                        "properties": {
                            "@@odata.type": {
                                "type": "string"
                            },
                            "id": {
                                "type": "string"
                            },
                            "displayName": {
                                "type": "string"
                            },
                            "userIdentityType": {
                                "type": "string"
                            },
                            "tenantId": {
                                "type": "string"
                            }
                        }
                    }
                }
            },
            "body": {
                "type": "object",
                "properties": {
                    "contentType": {
                        "type": "string"
                    },
                    "content": {
                        "type": "string"
                    }
                }
            },
            "attachments": {
                "type": "array"
            },
            "mentions": {
                "type": "array"
            },
            "reactions": {
                "type": "array"
            }
        },
        "required": [
            "id",
            "replyToId",
            "etag",
            "messageType",
            "createdDateTime",
            "lastModifiedDateTime",
            "lastEditedDateTime",
            "deletedDateTime",
            "subject",
            "summary",
            "chatId",
            "importance",
            "locale",
            "webUrl",
            "channelIdentity",
            "policyViolation",
            "eventDetail",
            "from",
            "body",
            "attachments",
            "mentions",
            "reactions"
        ]
    }
}

 

4-5-3.アレイのフィルター処理-システムメッセージを除去

取得したメッセージには「チャットにメンバー追加しました」「アプリのタブを追加しました」系のシステムメッセージも含まれますが、不要なものとして除去しています。

システムメッセージは"contnt"に<systemEventMessage/>が含まれていますのでこれを指定して除去しています。

 

4-5-4.選択-chat

取得したメッセージのアレイから、要約に必要な項目のみを抽出します。※チャットには件名はありませんが、チャネルのメッセージとアレイを共通化させるため、常にnullで設定しています。

 

4-5-5.作成-sortChatMessages

会話の時系列の流れを正しく認識させるためにメッセージを作成日時の昇順でソートします。(必ずしも必要ではないかもしれません)

sort(
    body('選択-chat'),
    '投稿日時'
)

 

4-5-6.変数の設定 messsages

ソートしたチャットメッセージを messages変数に格納します。

 

4-6.[いいえの場合]チャネル スレッドのメッセージを全部取得する

4-6-1.スレッドの親メッセージを取得

今回利用するGraph APIはスレッドの返信メッセージを取得するものですので、スレッドの親メッセージは別途取得し投稿日時・投稿者・本文・件名を変数に格納しておきます。

トリガー出力に含まれるメッセージでなく、「返信メッセージId」から親メッセージを取得する事で、スレッド内のどのメッセージの「…」メニューからフローを呼び出しても同じ結果となります。

4-6-2.スレッドの返信メッセージを取得

[カスタムコネクタ]

カスタムコネクタを使って、Graph APIで返信メッセージを取得します。

返信メッセージはこのようなjsonで返されます。

 

4-6-3.返信メッセージの変換

カスタムコネクタの出力をjsonの解析アクションでパースし、必要な情報のみを選択します。

JSONの解析アクションのスキーマ

{
    "type": "object",
    "properties": {
        "@@odata.context": {
            "type": "string"
        },
        "@@odata.count": {
            "type": "integer"
        },
        "value": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "replyToId": {
                        "type": "string"
                    },
                    "etag": {
                        "type": "string"
                    },
                    "messageType": {
                        "type": "string"
                    },
                    "createdDateTime": {
                        "type": "string"
                    },
                    "lastModifiedDateTime": {
                        "type": "string"
                    },
                    "subject": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "importance": {
                        "type": "string"
                    },
                    "locale": {
                        "type": "string"
                    },
                    "webUrl": {
                        "type": "string"
                    },
                    "from": {
                        "type": "object",
                        "properties": {
                            "application": {},
                            "device": {},
                            "user": {
                                "type": [
                                    "object",
                                    "null"
                                ],
                                "properties": {
                                    "@@odata.type": {
                                        "type": "string"
                                    },
                                    "id": {
                                        "type": "string"
                                    },
                                    "displayName": {
                                        "type": "string"
                                    },
                                    "userIdentityType": {
                                        "type": "string"
                                    },
                                    "tenantId": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    },
                    "body": {
                        "type": "object",
                        "properties": {
                            "contentType": {
                                "type": "string"
                            },
                            "content": {
                                "type": "string"
                            }
                        }
                    },
                    "channelIdentity": {
                        "type": "object",
                        "properties": {
                            "teamId": {
                                "type": "string"
                            },
                            "channelId": {
                                "type": "string"
                            }
                        }
                    },
                    "attachments": {
                        "type": "array"
                    },
                    "mentions": {
                        "type": "array"
                    },
                    "reactions": {
                        "type": "array"
                    }
                },
                "required": [
                    "id",
                    "replyToId",
                    "etag",
                    "messageType",
                    "createdDateTime",
                    "lastModifiedDateTime",
                    "importance",
                    "locale",
                    "webUrl",
                    "from",
                    "body",
                    "channelIdentity",
                    "attachments",
                    "mentions",
                    "reactions"
                ]
            }
        }
    }
}

 

4-6-4.作成-all messages

親メッセージを格納したmessages変数と、選択アクションの出力のアレイをunion関数で結合したのちに、sort関数で投稿日時の昇順に並び替えます。

作成-all messagesの式

sort(
    union(
        variables('messages'),
        body('選択')
    ),
    '投稿日時'
)

 

4-6-5.変数の設定-channel thread messages

ソートしたチャットメッセージを messages変数に格納します。

 

4-7.メッセージ本文のhtmiをプレーンテキストに変換

チャット・チャネルどちらもメッセージ本文(content)にはhtmlタグが含まれている為、「Htmlからテキスト」アクションでhtmlタグを除去しています。

アレイ変数を「Htmlからテキスト」アクションに指定する為、作成アクションを経由してjsonを張り付けています。


4-8.スコープ-AOAI Completion

4-8-1.パラメータの準備

[APIのパラメータ]

作成-system contentのsystemメッセージには下記のルールを設定しています。(要調整)

  • - Answer all questions in Japanese.
    • 回答は日本語で行う
  • - Summarize your answer within 200 characters.
    • 200文字程度に要約する
    • (2023/9/14追記)文字数の制約を削除しました
  • - Do not use " in your answers.
    • 回答には"を使用しない
    • ※"が含まれているとアダプティブカードに表示する際に加工が必要となるので使わないように指示しています
  • - Conversations should be interpreted as being in the order of date and time of posting.
    • メッセージは投稿日付順に解釈する
  • - Summarize your answers in up to 3 bullet points.
    • 3つ程度の箇条書きにまとめる
    • (2023/9/14追記)箇条書きの数の制限を削除しました

 

作成-user content は日本語のまま渡しています。英訳したほうが回答の精度が上がるようですが、一部の固有名詞が正しく解釈されなくなるケースが確認された為、あえて日本語で渡しています。

 

4-8-2.AOAI API call

[HTTPアクションの本文]

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

(2023/9/14追記) "max_tokens":4096 に増やしました

 

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

[JSONを解析のスキーマ]

{
    "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"
                }
            }
        }
    }
}

 

4-8-3.API実行結果で分岐

メッセージの内容によっては、AOAIのコンテンツフィルターによって回答がされない場合があります。回答がされない場合は"contents"が返されないため、条件分岐します。

条件の式

empty( 
    first(body('JSON_の解析')?['choices'])?['message']?['content'] 
)

正常な応答の例

 

フィルターに該当してしまった例

 

4-8-4.はいの場合

正常に回答が返ってきた場合は、Teamsのタスクモジュールで応答アクションで回答を表示します。アダプティブカードに渡す前に、回答に改行が含まれている可能性を想定して改行を\n\nに置換しています。

[作成-回答の式]

replace(first(body('JSON_の解析')?['choices'])?['message']?['content'], variables('改行'), '\n\n')

 

[Teamsのタスクモジュールで応答のアダプティブカード]

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "このスレッドではこんな事を話していました。"
        },
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "text": "@{outputs('作成-回答')}",
                    "wrap": true,
                    "id": "answer",
                    "height": "stretch",
                    "separator": true
                }
            ],
            "separator": true,
            "minHeight": "200px",
            "verticalContentAlignment": "Top",
            "height": "stretch"
        }
    ],
    "msteams": {
        "width": "Full"
    },
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3"
}

 

4-8-5.いいえの場合

正常な値が返されなかった場合はメッセージを表示して終了します。

[Teamsのタスクモジュールで応答のアダプティブカード]

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "選択したメッセージの概要"
        },
        {
            "type": "TextBlock",
            "wrap": true,
            "text": "AOAIで対応できない文章が検出されたか、何らかのエラーが発生しました。:@{first(body('JSON_の解析')?['choices'])?['finish_reason']}"
        }
    ],
    "msteams": {
        "width": "Full"
    },
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3"
}

 

今後の課題

  • アダプティブカードのメッセージは取得できていない
  • AOAIプロンプトが適当なので回答がイマイチな事がある
  • チャットの場合は、多くのメッセージを取得してしまうと過去の無関係な話題複数が含まれてしまうので、意味の無い要約になってしまうケースがある
  • Azure OpenAI をREST APIで呼び出している部分をAI Builderに置き換えたらフローがよりシンプルに(AI BuilderコネクタにGPTアクションが実装されたら…)

 

参考にしたページ

techcommunity.microsoft.com

 

yo-yon.hatenablog.com

 

yo-yon.hatenablog.com

 

learn.microsoft.com

learn.microsoft.com