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

ささみ学習帳

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

Outlookメールのメッセージを要約する Power Automate クラウドフロー 💎

 

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

指定したメールを要約して下書き返信メールの形で返します。

Copilt for Microsoft 365でもっとスマートに実現できるようですが、今自分が使える手段で実現してみました。

 

必要なもの

PowerAutomate Premium ライセンス

AI Builder GPTアクションを使用するためPowerAutomate Premiumライセンスが必要になります。

 

実行イメージ

 

例えば、突然何度も往復した後のメールが転送されてきた時に

 

スレッドに指定のカテゴリをセットします。

 

そしてトリガーメールにフラグをつけてフローを実行します。

※以前考えたフローの仕組みを使っています。

フラグが解除されたらフローの実行完了です。下書きにメール1通追加されています。

 

これまでのメールの経緯を要約した内容を確認することができます。

 

 

フロー全体図

フローの考え方

メール本文を取得して、AI BuilderのGPTアクションで要約しています。

カテゴリが設定されたメールを取得する処理と、要約結果を下書き返信メールとして出力する際にGraph APIを利用しています。

 

フロー解説

赤枠の部分のみ解説します。

その他部分は、メールのフラグを使って任意のフローを起動する処理です。

この部分についてはこちらの記事で解説しています。

sasami-axis.hatenablog.com

 

1.[データ操作] 作成-カテゴリ

このフローの処理対象のカテゴリを定義しています。

後続の処理でこの出力を使用します。

2.[変数]変数を初期化する-ReplyMessage

返信メールの本文を格納する変数です。

  • 名前
    • ReplyMessage
  • 種類
    • 文字列

    • 未設定

 

3.[コントロール]スコープ-削除済みアイテムフォルダのIdを取得

今回のフローは要約カテゴリがついたメールを取得して要約した結果を返信メールの下書きとして出力するフローです。ですが、削除済みアイテムには返信することができません。この為、検索から除外するために削除済みアイテムフォルダのIdを取得しています。

3-1.[Office 365 Outlook]HTTP要求を送信します-メールフォルダー一覧

Graph APIでメールフォルダの一覧を取得します。

learn.microsoft.com

結果はこのようなJSONで返されます。

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('1d8722fd-2174-4c63-be00-46191ddf03e8')/mailFolders",
  "value": [
    {
      "id": "AQMkADc2NWVmOGJlLTZmMDgtNGJlZi05OQA3ZS0wZTM3M2E5MjBjNDgALgAAAyc0t6Vx7LRGlfXIRdnvefoBAEuiNmqCqqdDqbqCmnf9MPwAAAIBCgAAAA==",
      "displayName": "削除済みアイテム",
      "parentFolderId": "AQMkADc2NWVmOGJlLTZmMDgtNGJlZi05OQA3ZS0wZTM3M2E5MjBjNDgALgAAAyc0t6Vx7LRGlfXIRdnvefoBAEuiNmqCqqdDqbqCmnf9MPwAAAIBCAAAAA==",
      "childFolderCount": 24,
      "unreadItemCount": 319,
      "totalItemCount": 1469,
      "sizeInBytes": 66057117,
      "isHidden": false
    }
  ]
}
3-2.[データ操作]アレイのフィルター表示-削除済みアイテム

3-1で取得したメールフォルダー一覧から"削除済みアイテム"フォルダーのみにフィルターします。

  • 差出人
    • @body('HTTP_要求を送信します-メールフォルダ一覧')?['value']
  • 条件
    • @{item()?['displayName']}
    • 次の値に等しい
    • 削除済みアイテム
3-3.[コントロール]条件-削除済みアイテムfolderIdが取得できなければ終了

削除済みアイテムフォルダーのIdが取得できない場合はフローを終了します。

  • 条件
    • @{length(body('アレイのフィルター処理-削除済みアイテム'))}
    • 次の値に等しい
    • 0

 

3-4.[データ操作]作成-削除済みアイテムfolderId

3-2.の出力から1つ目を抽出しておきます

  • 入力
    • @{first(body('アレイのフィルター処理-削除済みアイテム'))?['id']}

 

4.[コントロール]スコープ-対象メールを取得して要約

4-1.[Office 365 Outlook]HTTP 要求を送信します-メッセージを取得-要約カテゴリ付き

Office 365 Outlook コネクタの機能ではカテゴリを条件にメールが取得できない為、Graph APIを利用しています。

learn.microsoft.com

こちらについては別の記事でまとめています。

 

sasami-axis.hatenablog.com

結果はこのようなJSONで返されます。Office 365 Outlookコネクタのアクションよりも多くの情報が取得できます。

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('448060bd-fe71-424f-b70b-3456084c7ffd')/messages",
  "value": [
    {
      "@odata.etag": "W/\"CQAAABYAAACVD3WgHj8MTpdR6wepif3oAAC/FlQr\"",
      "id": "AAMkADcyZTA5MTgt---省略---3oAAAAAAEJAACMTpdR6wepif3oAAC_piGIAAA=",
      "createdDateTime": "2024-01-30T12:42:52Z",
      "lastModifiedDateTime": "2024-01-31T13:46:39Z",
      "changeKey": "CQAAABYAAACVD3WgHj8MTpdR6wepif3oAAC/FlQr",
      "categories": [
        "recap"
      ],
      "receivedDateTime": "2024-01-30T12:42:57Z",
      "sentDateTime": "2024-01-30T12:42:56Z",
      "hasAttachments": false,
      "internetMessageId": "<MN2P---省略--R06MB6606.namprd06.prod.outlook.com>",
      "subject": "Re: オンラインミーティングソフトウェアの障害について",
      "bodyPreview": "--------省略--------",
      "importance": "normal",
      "parentFolderId": "AAMkADcyZTJiYjIzLTA5MTgt---省略---3oAAAAAAEJAAA=",
      "conversationId": "AAQkADcyZTJiYjIzLTA5---省略---G9Ikx8DbACZKSI=",
      "conversationIndex": "AQHaU---省略---veAAAAZxA==",
      "isDeliveryReceiptRequested": false,
      "isReadReceiptRequested": false,
      "isRead": true,
      "isDraft": false,
      "webLink": "https://outlook.office365.com/owa/?ItemID=AAMkADcy---省略---3D&exvsurl=1&viewmodel=ReadMessageItem",
      "inferenceClassification": "focused",
      "body": {
        "contentType": "html",
        "content": "<html><head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">--------省略--------</html>"
      },
      "sender": {
        "emailAddress": {
          "name": "Sasami (ささみ)",
          "address": "sasami_axis@********.**.**"
        }
      },
      "from": {
        "emailAddress": {
          "name": "Sasami (ささみ)",
          "address": "sasami_axis@********.**.**"
        }
      },
      "toRecipients": [
        {
          "emailAddress": {
            "name": "アライ クマ",
            "address": "arai_kuma@********.**.**"
          }
        }
      ],
      "ccRecipients": [],
      "bccRecipients": [],
      "replyTo": [],
      "flag": {
        "flagStatus": "notFlagged"
      }
    }
  ]
}

 

4-2.[データ操作]JSON の解析-対象のメール

Graph APIの出力を解析し後続の処理で扱いやすくします。

  • コンテンツ
    • @{body('HTTP_要求を送信します-メッセージを取得-要約カテゴリ付き')}
  • スキーマ
    • {
          "type": "object",
          "properties": {
              "@@odata.context": {
                  "type": "string"
              },
              "value": {
                  "type": "array",
                  "items": {
                      "type": "object",
                      "properties": {
                          "@@odata.etag": {
                              "type": "string"
                          },
                          "id": {
                              "type": "string"
                          },
                          "createdDateTime": {
                              "type": "string"
                          },
                          "lastModifiedDateTime": {
                              "type": "string"
                          },
                          "changeKey": {
                              "type": "string"
                          },
                          "categories": {
                              "type": "array",
                              "items": {
                                  "type": "string"
                              }
                          },
                          "receivedDateTime": {
                              "type": "string"
                          },
                          "sentDateTime": {
                              "type": "string"
                          },
                          "hasAttachments": {
                              "type": "boolean"
                          },
                          "internetMessageId": {
                              "type": "string"
                          },
                          "subject": {
                              "type": "string"
                          },
                          "bodyPreview": {
                              "type": "string"
                          },
                          "importance": {
                              "type": "string"
                          },
                          "parentFolderId": {
                              "type": "string"
                          },
                          "conversationId": {
                              "type": "string"
                          },
                          "conversationIndex": {
                              "type": "string"
                          },
                          "isDeliveryReceiptRequested": {},
                          "isReadReceiptRequested": {
                              "type": "boolean"
                          },
                          "isRead": {
                              "type": "boolean"
                          },
                          "isDraft": {
                              "type": "boolean"
                          },
                          "webLink": {
                              "type": "string"
                          },
                          "inferenceClassification": {
                              "type": "string"
                          },
                          "body": {
                              "type": "object",
                              "properties": {
                                  "contentType": {
                                      "type": "string"
                                  },
                                  "content": {
                                      "type": "string"
                                  }
                              }
                          },
                          "sender": {
                              "type": "object",
                              "properties": {
                                  "emailAddress": {
                                      "type": "object",
                                      "properties": {
                                          "name": {
                                              "type": "string"
                                          },
                                          "address": {
                                              "type": "string"
                                          }
                                      }
                                  }
                              }
                          },
                          "from": {
                              "type": "object",
                              "properties": {
                                  "emailAddress": {
                                      "type": "object",
                                      "properties": {
                                          "name": {
                                              "type": "string"
                                          },
                                          "address": {
                                              "type": "string"
                                          }
                                      }
                                  }
                              }
                          },
                          "toRecipients": {
                              "type": "array",
                              "items": {
                                  "type": "object",
                                  "properties": {
                                      "emailAddress": {
                                          "type": "object",
                                          "properties": {
                                              "name": {
                                                  "type": "string"
                                              },
                                              "address": {
                                                  "type": "string"
                                              }
                                          }
                                      }
                                  },
                                  "required": [
                                      "emailAddress"
                                  ]
                              }
                          },
                          "ccRecipients": {
                              "type": "array"
                          },
                          "bccRecipients": {
                              "type": "array"
                          },
                          "replyTo": {
                              "type": "array"
                          },
                          "flag": {
                              "type": "object",
                              "properties": {
                                  "flagStatus": {
                                      "type": "string"
                                  }
                              }
                          }
                      },
                      "required": [
                          "@@odata.etag",
                          "id",
                          "createdDateTime",
                          "lastModifiedDateTime",
                          "changeKey",
                          "categories",
                          "receivedDateTime",
                          "sentDateTime",
                          "hasAttachments",
                          "internetMessageId",
                          "subject",
                          "bodyPreview",
                          "importance",
                          "parentFolderId",
                          "conversationId",
                          "conversationIndex",
                          "isDeliveryReceiptRequested",
                          "isReadReceiptRequested",
                          "isRead",
                          "isDraft",
                          "webLink",
                          "inferenceClassification",
                          "body",
                          "sender",
                          "from",
                          "toRecipients",
                          "ccRecipients",
                          "bccRecipients",
                          "replyTo",
                          "flag"
                      ]
                  }
              }
          }
      }

 

4-3.[コントロール]条件-エクスポートするメールがなければ終了

  • "@length(body('JSON_の解析-対象のメール')?['value'])"
  • 次の値に等しい
  • 0

Graph APIでメールが1件も取得できなかった場合にはフローを終了しています。

 

4-4.[Content Conversion]HTMLからテキスト

1つめのカテゴリ付きメールのメッセージ本文をテキストに変換します。

自分の用途では複数メールを一度に要約するシーンは想定していないのでこのようにしています。

Outlook on the Webでメールにカテゴリを設定する動作はメッセージの表示方法で動作が異なるようです。メールにカテゴリを追加する操作を行ったとき、スレッド表示の場合はスレッド全体に、個別のメッセージ表示の場合は表示中のメールにのみカテゴリが追加されます。この動作がありますので、複数のメールを一括で要約することは難しいと判断し、1つ目のメールのみを対象としています。

  • コンテンツ
    • @{body('JSON_の解析-1つ目のメール')?['body']?['content']}

 

4-5.[AI Builder]GPT でプロンプトを使用してテキストを作成する

  • プロンプト
    • AIプロンプトのプロンプトビルダーであらかじめカスタムプロンプトを作成しておきます
    • Follow the steps below to process the text. " input text " 
      
      #Step1 
      - Split emails by delimiter 
      -Delimiter:---------------------------------------------- ---------------------------------- 
      
      #Step2 
      Sort emails by oldest sent date and time 
      
      #Step3 
      Summarize the text below in less than two paragraphs without adding new information. When the text below seems too short to make at least one paragraph of summary, answer that you can't summarize a text that is too short. Output must be in Japanese
    • プロンプト解説
      • Outlookの送受信履歴を引用したメールをテキストに変換すると、"------略-----"で区切られた状態で、送信日時の降順に並んでいます。このまま処理すると、時系列を無視して上から文章が要約されてしまうため、要約結果もおかしな状態になりがちでした。そこで3ステップで処理を行うように指示しています。
        • まず区切り文字ごとに分割します
        • 次にメールの送信日時を基準に昇順に並び替えます
        • そして要約を行います。Step3のプロンプトはデフォルトの「テキストを要約する」に日本語で出力する指示を付け加えたものです。
  • Input SourceText
    • @{body('Html_からテキスト')}

 

4-6[コントロール]条件-GPTアクションの結果で分岐

GPTアクション出力の状態コードが200の場合正常に結果が返ってきています。
200以外の場合は何らかの異常(よくあるのはGPTのフィルターに抵触してエラー)が起きた状態です。

  • 条件
    • @{outputs('GPT_でプロンプトを使用してテキストを作成する')['statusCode']}
    • 次に等しい
    • 200
  • ※実行条件の構成で失敗した場合にもこのアクションが実行されるようにしておきます。GPTアクションが失敗した場合エラーとなってしまうためこのようにしています。
4-6-1[変数]変数の設定-ReplyMessage@正常

GPTアクションの出力が200だった場合は、出力からテキストを抽出します。

今回はメールに出力するため、開業が含まれる場合は<p>に変換しています。

  • 名前
    • ReplayMessage
    • <p><span>🤖<元メールを要約するとこんな感じです。<br>
      </span></p>
      <p>@{replace(
          replace(
          outputs('GPT_でプロンプトを使用してテキストを作成する')?['body/responsev2/predictionOutput/text'],
          '\n',
          '</p>\n<p>'
          ),
          '- ','<li>')
      }</p>
4-6-2[変数]変数の設定-ReplyMessage@異常

GPTアクションの結果が200以外の場合は、出力されるJSONのbodyをそのまま出力しています。

 

  • 名前
    • ReplayMessage
    • <p><span>🤖<メール処理中に問題が発生しました。</span></p><p>@{body('GPT_でプロンプトを使用してテキストを作成する')}</p>

 

5.[コントロール]スコープ-メールから要約カテゴリを除去

メールから要約カテゴリを除去します。

カテゴリはこのように複数設定が可能な値ですので、各メールが保持していたカテゴリからフィルター処理で要約カテゴリを除いた分類のセットを作って更新しています。

5-1.[コントロール]Apply to each

  • 以前の手順から出力を選択
    • @{body('JSON_の解析-対象のメール')?['value']}

 

5-2.[データ操作]アレイのフィルター処理-要約カテゴリを除去

  • 差出人
    • @items('Apply_to_each')?['categories']
  • フィルター条件
    • @{item()}
    • 次の値に等しくない
    • @{outputs('作成-カテゴリ')}

 

5-3.[Office 365 Outlook]HTTP要求を送信します-カテゴリを更新

 

6.[コントロール]スコープ-元メールに返信する下書きを作成

GPTアクションの出力を下書き返信メールとして出力します。

あえて返信メールの下書きで保存しているのには理由があります。要約した結果は一度見たら削除する運用を想定しています。普通に元メールにto自分で返信すればOffice 365 Outlookコネクタのアクションで実現はできます。

しかし、その場合は要約メールを削除する際に受信したメールと送信済みメールの2通を削除する手間が発生します。より容易に削除しできる下書きメールでの保存としました。

下書きメールの作成はGraph APIを使っています。この部分についてはこちらの記事で解説しています。

sasami-axis.hatenablog.com

 

 

6-1.[Office 365 Outlook]HTTP要求を送信します-返信の下書きを作成

 

6-2.[Office 365 Users]マイ プロフィールの取得 (V2)

メールの返信先アドレスに使用します。うっかり要約メールをご送信してしまわないように、メールの宛先を自分のメールアドレスで上書きしておくためです。

6-3.[Office 365 Outlook]メールに返信する (V3)

  • メッセージID
    • @{body('JSON_の解析-1つ目のメール')?['id']}
  • 本文
    • @{variables('ReplyMessage')}
    • ※コードビューにしておく

 

 

このフローの制限事項・改善したい点

  • 自分のメールボックスのメールのみ対応
    • 今回のGraph APIの使い方では自分のメールボックスのみ取得できます。
  • 要約の精度
    • プロンプトを見直すことでもう少し改善できそうです
  • GPTアクションのエラー処理
    • エラーの場合も続行させているところが要改善

 

さいごに

Copilot for Outlook (でいいのか?)で実現できるメールの要約を行ってみました。フラグやカテゴリを使ってUIを工夫してはみましたが、機会があればCopilot for M365の要約と比較してみたいです。

このフローは元々はAzure OpenAI Service を使ったフローでしたが、プロンプトビルダーがGAされたとのことで切り替えてみました。シンプルに使えてよい機能ですね。