PowerAutomateフローでTeamsメンバーをランダムに選出するロジックってどうやってますか？

こんにちは。（そして初めまして）
@tariki-code こと[コバッチ](https://twitter.com/kobatch_tk)と申します。
Qiita Advent Calendarへの参加ということで、久々にQiita記事を書きたいと思います。


## 簡単に自己紹介

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/a0efe889-2dec-fd63-20f9-815f45dc95b9.jpeg" width=180>

* （アイコンとのギャップがありますが。。）40代のオジサンです。
* 90年代後半からIT業界に関わる。当時はCOBOLやVB（ちょっと珍しいところでは「M」ってのも。。）なんかのプログラミングに携わる。
* JavaやPHPや.NETとか色々やってきたが、近年はプライム上場企業やスタートアップでNode.jsやVueやReactなどフロントエンドのフレームワークを使ったプロダクト開発、およびそのエンジニア組織のMGR、PMなんかを務める。
* 2023年度より某中央官庁でDX推進担当として起用される。初めて行政の世界に身を投じる。
* TeamsもPowerplatformもそこで初めて出会ってやってみたということで・・・ここに関してはピカピカの1年生です。
* 先日JPPC2003メインイベントに登壇させていただきましたー（詳細はYouTube等で）

https://powerplatformconf.connpass.com/event/294851/

## 今回の課題

PowerAutomateで、Teamsのチーム参加メンバーから任意の人数をランダムに選出したいという処理を入れたい時はありませんか？

具体的なユースケースですが、私は以下の機能を実装する中で「どうやって実現しようか」と思慮しました。


### Teamsでコミュニケーション活性化の為、アイスブレイクを仕組み化する機能

Teamsにて部署横断的なチームを運営しており、その中で実現した機能です。
処理はザッとこんな感じです。

* Teams上の対象チーム参加者（200名程度）の中から**ランダムに5名を選出**する。
* 選出された5名には（別途、SharepointListで保存している）質問からランダムに選択し、AdaptiveCardを使ったフォーム形式で質問をDMにて投げかける。
    * 例） ※業務とは関係のない雑談的なもの
        * 昨日晩御飯何食べた？
        * 最近買ったお気に入りは何ですか？
        * 周辺のオススメランチを教えてください・・・など。
* 回答内容もSharepointListで保存し、お昼休みのタイミングでチーム内の公開用チャンネルでシェアする。

#### イメージ① - チームの参加メンバーにAdaptiveCardを使ってDMを送る

選出者にはこんなDMが来ます。（「リクエストありがとうございます」・・・っておかしいですが、自分で呼び込める機能もあり、そちらの画像を出しちゃいました。。）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/8a8e18de-a1a0-ea87-a6d1-01ef2db9b421.png" width=350>

#### イメージ② - 回答内容を同じくAdaptiveCardを使って専用のチャンネルでシェア

回答内容はこんな感じでシェアされます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/eaf3b376-f219-8076-9e6e-b1388c40afc1.png" width=430>


### その他要件

やりたいことは200名ほどいるTeamsのチームメンバーの中からランダムに5人を選出するということですが、そこに状況に応じた以下の要件もありました。

* スレートレスな処理にしたい。
    * 対象としているTeamsのチームは部署横断的に参加しているいわばコミュニティ用のチームであるため、自由に出入りすることができ、メンバーの増減が激しい。そのため、例えばメンバーリストを別管理するなど手運用や依存する処理を増やさず、APIからリアルにその時点の情報を取得するなどで、変動に影響を受けない処理にしたい。
* 一定期間は選出されないようにしたい。
    * あくまで雑談をするための処理なので、一度選出された人は1ヶ月程度は選出されないようにしたい。（業務にあまり支障のない範囲で楽しんでもらうかつ、あまりDMがうるさい感じになるとそれが嫌気となる懸念がある為）


### 実現に向けて具体的な処理を考えなければいけない

今回フォーカスを当てている処理は単純で些細なものなのですが、意外と他の記事やネットで具体的なロジックや実装方法って語られていないと思いました。
（「ここで、ランダムに選択します」・・・・以上！みたいな）

もしくは記載があったとしてもバッチリ自分のやりたいケースに当てはまる・・・というものを見つけにくい。

Excelを活用するなんて例は拝見しました。

https://zenn.dev/canonno/articles/5fb62926140e4b

### 些細な処理の共有がナレッジの積み上げにつながる

似た目的でもPowerAutomateを使った実装方法って様々なやり方があるかと思います。
自分のケースと考えた実装例を共有していくことが、誰かの今後の解決手段のヒントにもなるという考えもあるかな。

・・・ということで、どちらかと言えばエッジケースな処理ですし、良いやり方かどうかはわかりませんが、私が実装した内容を共有したいと思います！

そして、もっといいやり方あるよということがあればぜひ教えていただければありがたいです！！

## 実装した内容

シンプルにいうと以下の通り実現しました。

* Teamsの該当チームのメンバー取得。
    * Office365 Groupsで「グループメンバーの一覧表示」でメンバーリストを取得。
* メンバーリストからランダムに5人選出。
    * 取得したメンバーリストでApplyToEachし、`rand`関数で対象とするか判定。
* 選択されたメンバーの管理。
    * SharepointListにて履歴管理を行う。

### Office365 Groupsで「グループメンバーの一覧表示」でメンバーリストを取得

これは簡単なことですが、Teamsの対象チームのメンバーリストは、Office365 Groupsで「グループメンバーの一覧表示」で取得します。

グループIDの項目で対象のTeamsチームを指定するだけで取得できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/48383717-fa39-12a4-42b1-09d19227b0ce.png" width=550>


併せて、合計何件取得できたのか？その数を`length`関数を使って変数に保存しておきます。
（`sub`関数は1件除外したいだけなので、気にしないでください。。）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/1a005737-3205-92da-68c7-ce0745996607.png" width=550>



### 取得したメンバーリストでApplyToEachし、`rand`関数で対象とするか判定

上記で取得したメンバーリストを使ってループ処理を行います。
その中で、`rand`関数を使って選出するか判定するロジックを入れました。

具体的には「条件」コントロールで以下の処理を入れています。
（真ん中のUPNの条件は環境固有の事情なので気にしないでください。。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/d48366d0-0d1f-6a9f-0f15-423275f80d44.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/78e8efa1-17d9-eae4-099b-d70e5dced4c6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/190c8366-c682-eaf2-7ee0-31809de43d40.png)

下記のような条件としています。

```js

// ループの都度0からメンバー数までの数字をランダムで選択してもらい、その数が190以上であれば対象
rand(0, outputs('作成')) >= sub(output('作成'), 10)

```

かつ

```js

// 選出された都度インクリメントされる変数（sendListCount）が５以内であれば対象
variables('sendListCount') <= 5

```

チームのメンバーがおおよそ200人、そのループの処理をする毎に、0から200までの数字で乱数を取得する、その結果が`200-10=190`以上であれば対象とする。すなわち、理論的には5％程度の確率で発生する・・・ということになります。

10（というマジックナンバー笑）は、チーム内の参加メンバーが少ないうちはもっと小さい値にしていたのですが、200名くらいになると全然発生しないケースもあったので、その後のロジックで対象外とされることも鑑みて広めに調整しました。


### 取得結果をSharepointListに保存し履歴管理を行う

過去31日に選出された人には送りたくないので、過去の選出履歴と突合します。
履歴の管理にはSharepointListを使うことしました。

リストの項目は以下です。このリストにいる人は過去31日に選出された実績がある・・・という意味のリストです。
すなわち、ここにいない人が選出対象ということになります。

* UPN
* 選出された日

この整合性を保つべく、まずはフローの最初で、31日以上経過した人をリストから削除します。Sharepointの「複数の項目の取得」から、項目として設定している前回選出された日付が31日以前のものを指定して取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/49e7bf5d-b590-42ea-6603-4affabb205a3.png)

この後は取得したリストをApplyToEachで1件づつ削除します。

次に、上記のループ処理の中で、乱数で選択された人の中から、過去31日以内に選出された履歴がある人を除外します。
リストにある人＝過去31日間に選出された人なので、このリストに存在していない人が対象です。

Sharepointの「複数の項目の取得」から対象のリストに対してUPNで検索をかける。その結果が0件であれば過去31日に選出されていない・・・という判断になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/f9867fa0-d48f-847b-0f09-a4f9b35a7ec9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194056/84d61677-d276-a4e8-36eb-e26b0344fa3e.png)

最終的に選択された人を新たに選出された人として、リストに保存します。
こちらはSharepointの「項目の作成」で実施をします。

ループの中で、選出された人を管理するために変数をインクリメントします。
こちらは先ほどの`sendListCount`なので、変数コントロールで実施します。

### 全体をザッとフローにすると

こんな感じです。
（他の色んな処理は全て割愛して、説明した箇所だけのものになります）

```mermaid

flowchart TB

X[start]
A[SharepointListから31日以上経過した人の削除]
B[Teamsメンバーリスト取得 - グループメンバーの一覧表示]
C[/ApplyToEach - Start\]
G[メンバーリスト1件読み込み]
id1{rand関数結果190以上かつ選出済5件以内}
id2{SharepointListに存在している}
D[\ApplyToEach - End/]
E[選出対象としてSharepointListへの追加]
F[sendListCountのインクリメント]
T[対象者へDM発出処理]
Z[End]

X --> A
A --> B
B --> C
C --> G
G --> id1
id1 -->|No| D
id1 -->|Yes| id2
id2 -->|Yes| D
id2 -->|No - 対象確定| E
E --> F
F --> D
D -->|Next| C
D -->|End| T
T --> Z

```

・・・こんな感じですがいかがでしょうか？？
乱数で必ず選択できる訳ではないので、正確には0以上最大５人という処理仕様になっています。。笑

正確性を求める処理には不向きかもしれませんが、こういうコミュニティの中で自由に動かすBotであれば多少の不確実性があった方が人間味があって良いかなと思っています。（言い訳笑）

## 今後の課題

### 「グループメンバーの一覧表示」の上限問題

「グループメンバーの一覧表示」は取得できる最大件数が999までとなっています。
なので、Teamsメンバーがそこに収まっているうちは特段何も対応する必要がありませんが、それを超える場合は別のやり方を考えないといけません。。

どう実現するかは今のところノーアイデアです。。

### 実行時間の問題

今回は重視するポイントとしてはTeamsのチームメンバーの頻繁な変遷に動じることなく、安定して処理ができるという点なのですが、取得したメンバーリストをループで1件づつ処理をさせるので性能という観点では決して良いものではありません。

メンバーが増えれば増えるほど遅くなります。ただ、今回に関してはユーザーがその処理を待っている訳ではなく、トリガーは別で発動され、DMが送られて初めて気づくものであるので、その辺りは重視をしませんでした。

ユーザーがトリガーを発動して終了を待つようなものであれば、実行時間の遅延がストレスになるので不向きかもしれません。

### 発生確率の調整問題

唯一メンテナンスが必要であるとするなら、この点になります。
10（のマジックナンバー）は固定されているので、メンバーが増えれば増えるほど選出される確率が下がってきます。

* 現在(200人)
    * `200-10=190`以上、すなわち発生確率5%
* 300人になると
    * `300-10=290`以上、すなわち発生確率3.33...%
* 500人になると
    * `500-10=490`以上、すなわち発生確率2%

この辺りは人数が増えても一定の確率になるような調整をするロジックが入っても良いかもしれません。
というかこれくらいはやっておかないといけないですね。。（近いうちに修正）

## まとめと感想

具体的なユースケースによる細かい処理について話しましたが、実際のところ、こういう具体的なやり方やロジックの組み方で躓いたり悩んだりすると思うので、こういう粒度のやり方やナレッジを共有することには一定の意義があると思いました。

### まとめ

* Teamsの参加メンバー取得にはOffice365 Groupsで「グループメンバーの一覧表示」を使う。（最大999人まで）
* ループ処理の中で参加人数に応じた`rand`関数の判定を行うことで、いい感じの選出ができるようになる。（但し、確実に５人選ぶような処理にはならない）
* 一定期間は選出できないようにするためにはSharepointListなど別で保管するしかない、但し一連の処理の中で整合性を保つ処理を実装することは可能。
* 他にもきっともっといいやり方があるはず。

### 感想

* 「気ままに勉強会」で勉強させていただいています。今回の機能もTeamsコネクタ周りとか多くを参考にさせていただきました、ありがとうございます！
* PowerAutomate（というかRPA関連の）記事を書くって難しい。。笑
* この粒度で困ったこと悩んだこと色々あったので、もっと自分が悩んだことやったことを共有し、もっといいやり方なんかを収集したい！！
* クリスマスまで後7日、楽しみですね、素敵なクリスマスをお過ごしください〜。そして来年もPowerAutomate頑張ってやっていきたいと思います！！

https://kimamani.connpass.com/