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

ささみ学習帳

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

Teams チャネルのスレッドの内容を3行程度に要約する Power Automate クラウドフロー💎

(2023/8/15 21:20追記)当初beta版 Graph APIで動作確認していましたが、正式版でも正常に動作することを確認できましたので修正しました。

 

Azure OpenAI Service(以降AOAI)が検証に利用できるようになったので、Teams Copilotでこんな機能できるようになるのだろうなーと作ってみました。

 

 

実現できること

  • Teams チャネルの特定のスレッドでこれまで会話された内容をざっくり3行程度に要約する



実行イメージ

チャネルのメッセージを選択してフローを実行すると、スレッドの要約が表示されます。

 

今回フローはチャットには対応していないので、チャットから実行するとメッセージが表示されます。

 

必要なもの

  • Microsoft Teams, PowerAutomate
    • 職場または学校アカウントでの利用です
  • Azure OpenAI Serviceが利用可能であること
    • 今回はgpt 3.5 turbo 16kを使っています
    • ごくごく初歩的な使い方なのでOpenAI APIでもほぼ同じ事が実装可能です。
  • Entra ID (Azure AD)にアプリケーション登録ができること
    • Graph API利用の為にアプリ登録が必要です
  • Power Automate プレミアムコネクタが利用可能であること
    • 有償ライセンスもしくは検証用途であればPowerApps開発者プラン
    • HTTPコネクタおよびカスタムコネクタを利用するため必要になります

 

準備

1.カスタムコネクタの作成

スレッドの全メッセージを取得する必要がありますが、Teamsコネクタにはそれを実現できる機能はありません。そのため、Graph APIを利用します。

 

チャネルメッセージの応答を一覧表示する

GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies

learn.microsoft.com

----------(2023/8/15 21:20追記ここから)----------

当初beta apiで動作確認していましたが、正式版のv1.0の方でも正常に動作することを確認できました。

----------(2023/8/15 21:20追記ここまで)----------

カスタムコネクタは、Techcommunityのこちらのページのカスタムコネクタを利用した。リンク先ページではbeta apiでの説明となっていますが、APIのエンドポイントを正式版を下記のように読み替えて作成しています。

GET https://graph.microsoft.com/beta/teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies

 

GET https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies

 

techcommunity.microsoft.com

 

 

2.AOAI の準備

Azure OpenAI Studioで以下を行います。

  1. 管理→デプロイでgpt-3.5-turbo-16k をデプロイ
  2. プレイグラウンド→チャットで
    1. デプロイを選択
    2. システムメッセージを設定
    3. 適当なユーザーメッセージを設定
    4. コードを表示→json
      1. jsonをコピーしておく
      2. エンドポイントをコピーしておく
      3. キーをコピーしておく

 

フロー全体図

 

フロー解説

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

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

 

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

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

 

3.チャットからフローを呼び出した場合はメッセージを表示して終了する

選択したメッセージの場合トリガーは、チャネルだけでなくチャットの「…」メニューにも表示されます。今回のフローはチャネルからの呼び出しのみを想定していますので、チャットからの呼び出しの場合はメッセージを表示して終了します。

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

[条件の式]

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

チャットからの呼び出しの場合はTeamsのタスクモジュールで応答アクションでメッセージを返してフローを終了しています。

 

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

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "size": "Medium",
            "weight": "Bolder",
            "text": "選択したスレッドをざっくりまとめる"
        },
        {
            "type": "TextBlock",
            "wrap": true,
            "text": "このフローはチャットには対応していません。"
        }
    ],
    "msteams": {
        "width": "Full"
    },
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.3"
}

 

4.スレッドのメッセージを全部取得する

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

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

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

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

[カスタムコネクタ]

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

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

 

[返信メッセージの変換]

カスタムコネクタの出力を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"
                ]
            }
        }
    }
}

 

[親メッセージを格納した変数messagesと結合してソート]

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

作成-all messagesの式

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

 

[メッセージ本文のhtmiをプレーンテキストに変換]

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

 

5.AOAIでスレッドのメッセージを要約する

5-1.AOAI chat completionを呼び出す

[APIのパラメータ]

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

  • 回答は日本語で行う
  • 200文字程度に要約する
  • 回答には"を使用しない
  • 3つ程度の箇条書きにまとめる
  • メッセージは投稿日付順に解釈する

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

[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": 800,
  "stop": null
}

 

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

 

5-2.API実行結果で分岐

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

条件の式

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

正常な応答の例

 

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

 

5-3.はいの場合

正常に回答が返ってきた場合は、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"
}

 

5-4.いいえの場合

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

[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プロンプトが適当なので回答がイマイチな事がある
  • Graph API 「チャネルメッセージの応答を一覧表示する」がv1.0の方では動作しないのでbetaを使っている点

 

余談-Office 365 Group コネクタのHTTP要求を送信しますアクション😖

今回使用したGraph API「チャネルメッセージの応答を一覧表示する」は、Office 365 Group コネクタの「HTTP要求を送信します」アクションで呼び出す事が可能でした。フロー作成中に同アクションが表示されなくなったため、近いうちに使えなくなる可能性も踏まえカスタムコネクタに切り替えました。手軽にGraph APIが利用できるいい奴でした。残念です。

 

参考にしたページ

techcommunity.microsoft.com

 

learn.microsoft.com

 

 

続く

その後改良版を作ってみました。

こちらのページに続きます。

sasami-axis.hatenablog.com