PowerAutomate: LLM で Office Scripts を作らせれば、リアクション集計も簡単に

# 背景

LLM が浸透してきたことで、
ようやく会社で GitHub Copilot が使えるようになりました。

ってことで、Power Automate も自動生成活用方法を考えてみた

# 結論

- Office Scripts を LLM に生成させれば、Power Automate での整形とかが楽になる

さすがに、Power Automate の JSON を生成することはできなかったけど・・ :laughing: 

## 具体的なイメージは？

1. Trigger
1. なんらかのデータ取得
1. Run (office) Script
    - LLM でここの script を作らせるだけ
    - ただし、動作数の上限はあるので、稼働させ過ぎには注意
1. Teamsへ投稿したりして活用

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1645569/0e3548ac-5f7f-4acb-b54a-b856bb63ac5d.png)


## Adaptive Card とは？

以下にアクセスするのが一番分かり易い。UI を Json で提議するもの。と思えば OK

https://www.adaptivecards.io/designer/

# あとがき

勉強会の参加者を、特定のアイコンで募集するとき用に作ろうとして、Adaptive Card を Automate で生成するのが面倒だったので、LLM に任せてみたら簡単にできたのでした。
以前までは、Column Sets を 配列で生成してたんで面倒だったんですけどね :sweat: 

---

以下、サンプルとしての実装例
LLM で、Office Scripts と Adaptive Card を作ってもらう感じ

# 具体的な例としての、リアクション集計の実装

## 動作イメージ

チャットや、チャネルでのメッセージについている リアクションの集計をして、リアクションした人の名前と Emails 一覧を表示する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/2137089e-a4ab-4e0b-9558-e539b9485fe2.png)


こんな感じで、リアクションの集計をする。
Email のところをクリックして emails list をコピーすれば、以下のようなことができる
- 特定のリアクションをした人だけの会議招集や、チャットなどに使える
- Polls を使わずに、簡単なリアクション集計

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/40a541c0-c7b6-4e1c-8207-0fc547534e6f.png)

## リアクション集計実装イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120072/014a90c4-3ef5-4683-a802-83762544a325.png)


## For a selected message 

```json
{
    "type": "AdaptiveCard",
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.4",
    "msteams": {
        "width": "full"
    },
    "body": [
        {
            "type": "TextBlock",
            "text": "リアクション集計をします"
        },
        {
            "text": "Cancel は右上の ☒ を",
            "type": "TextBlock",
            "size": "Small",
            "horizontalAlignment": "Right",
            "weight": "Lighter",
            "color": "Accent"
        }
    ]
}
```

## Office Scripts

```typescript
interface IUser {
    UserID: string;
    DisplayName: string;
    EMail: string;
}

interface IReaction {
    reactionType: string;
    userId: string;
    userName: string;
    reactionName: string;
}

interface MergedUser {
    userId: string;
    userName: string;
    displayName: string;
    email: string;
}

// ReactionGroup に reactionName を追加
interface ReactionGroup {
    reactionType: string;
    reactionName: string;
    users: MergedUser[];
}

interface OriginalMessage {
    Message: string;
    LinkToMessage: string;
}

function main(
    workbook: ExcelScript.Workbook,
    reactionsString: string = '[{"reactionType":"🚢","userId":"userid-0001","reactionName":"Ship"}]',
    usersString: string = '[{"UserID":"userid-0001","DisplayName":"たかにぃ","EMail":"hoge@hoge.onmicrosoft.com"}]',
    originalMessageJSON: string = '{ "Message": "Default quoted message", "LinkToMessage": "https://teams.microsoft.com/l/message/DEFAULT" }'
): string {
    // Reaction に対応する画像 URL のマッピング
    const reactionImageMap: { [key: string]: string } = {
        "yes": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/yes/default/20_f.png",
        "like": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/yes/default/20_f.png",  // like > yes となった過去の経緯により
        "heart": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/heart/default/20_f.png",
        "laugh": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/laugh/default/20_f.png",
        "surprised": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/surprised/default/20_f.png",
        "sad": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/sad/default/20_f.png",
        "angryface": "https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/angryface/default/20_f.png"
    };

    let parsedUsers: IUser[];
    try {
        parsedUsers = JSON.parse(usersString);
    } catch (error) {
        console.log("ユーザー情報のJSONパースに失敗しました", error);
        return "";
    }
    const userDict: { [key: string]: IUser } = {};
    parsedUsers.forEach((user: IUser) => {
        userDict[user.UserID.toLowerCase()] = user;
    });

    let reactions: IReaction[];
    try {
        reactions = JSON.parse(reactionsString);
    } catch (error) {
        console.log("反応情報のJSONパースに失敗しました", error);
        return "";
    }

    // reactionName をキーにグループ化する
    const grouped: { [key: string]: { userId: string; userName: string; reactionType: string }[] } =
        reactions.reduce((acc, reaction: IReaction) => {
            const key: string = reaction.reactionName;
            if (!acc[key]) {
                acc[key] = [];
            }
            acc[key].push({
                userId: reaction.userId ? reaction.userId.toLowerCase() : "",
                userName: reaction.userName,
                reactionType: reaction.reactionType
            });
            return acc;
        }, {} as { [key: string]: { userId: string; userName: string; reactionType: string }[] });
    // ReactionGroup は、表示時は従来通り「reactionType / reactionName (件数)」として出す

    const result: ReactionGroup[] = Object.entries(grouped)
        .sort((a, b) => b[1].length - a[1].length)
        .map(([reactionName, reactionsArray]) => {
            const first = reactionsArray[0];
            const mergedUsers: MergedUser[] = reactionsArray.map((r) => {
                const info: IUser | undefined = userDict[r.userId];
                return {
                    userId: r.userId,
                    userName: r.userName,
                    displayName: info ? info.DisplayName : "",
                    email: info ? info.EMail : ""
                };
            });
            return { reactionType: first.reactionType, reactionName, users: mergedUsers };
        });
    console.log(result);

    // Adaptive Card の JSON 構築
    let card: Record<string, unknown> = {
        type: "AdaptiveCard",
        $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
        version: "1.4",
        msteams: { width: "full" },
        body: [] as unknown[]
    };

    // 元メッセージ表示（引用）
    let originalMsg: OriginalMessage;
    try {
        originalMsg = JSON.parse(originalMessageJSON);
    } catch (error) {
        console.log("元メッセージのJSONパースに失敗しました", error);
        originalMsg = { Message: "元メッセージが取得できませんでした", LinkToMessage: "" };
    }
    card.body.push({
        type: "Container",
        style: "emphasis",
        items: [
            {
                type: "TextBlock",
                text: originalMsg.Message,
                wrap: true,
                isSubtle: true
            },
            {
                type: "TextBlock",
                text: `[元のメッセージを表示](${originalMsg.LinkToMessage})`,
                wrap: true,
                size: "Small",
                color: "Accent"
            }
        ]
    });

    // タイトルブロック
    card.body.push({
        type: "TextBlock",
        text: "Reactions Summary",
        weight: "Bolder",
        size: "Large"
    });

    // テーブルヘッダ (3列: Reaction, User Name, Email)
    card.body.push({
        type: "ColumnSet",
        columns: [
            {
                type: "Column",
                width: "stretch",
                items: [
                    {
                        type: "TextBlock",
                        text: "Reaction",
                        weight: "Bolder",
                        wrap: true
                    }
                ]
            },
            {
                type: "Column",
                width: "stretch",
                items: [
                    {
                        type: "TextBlock",
                        text: "User Name",
                        weight: "Bolder",
                        wrap: true
                    }
                ]
            },
            {
                type: "Column",
                width: "stretch",
                items: [
                    {
                        type: "TextBlock",
                        text: "Email",
                        weight: "Bolder",
                        wrap: true
                    }
                ]
            }
        ]
    });

    // グループごとの行データ作成部分
    result.forEach((group: ReactionGroup, idx: number) => {
        // Reaction 列: 画像＋テキスト（reactionType / reactionName (件数)）
        const lowerType: string = group.reactionType.toLowerCase();
        let reactionColumnItems: Record<string, unknown>[] = [];
        const reactionText = `${group.reactionType} / ${group.reactionName} (${group.users.length})`;
        if (reactionImageMap[lowerType]) {
            reactionColumnItems.push({
                type: "Image",
                url: reactionImageMap[lowerType]
            });
            reactionColumnItems.push({
                type: "TextBlock",
                text: reactionText,
                wrap: true
            });
        } else {
            reactionColumnItems.push({
                type: "TextBlock",
                text: reactionText,
                wrap: true
            });
        }
        // ユーザー情報（DisplayName）を改行区切りでまとめる
        const userNames: string = group.users.map(u => u.displayName).join("\n");
        // メールアドレスはコピー用のデータとして取得しておく
        const userEmails: string = group.users.map(u => u.email).join("\n");

        // 行は3列とする：Reaction / User Name / Email(コピー用ボタン)
        card.body.push({
            type: "ColumnSet",
            columns: [
                {
                    type: "Column",
                    width: "stretch",
                    items: reactionColumnItems
                },
                {
                    type: "Column",
                    width: "stretch",
                    items: [
                        {
                            type: "TextBlock",
                            text: userNames,
                            wrap: true,
                            size: "Small"
                        }
                    ]
                },
                {
                    type: "Column",
                    width: "stretch",
                    items: [
                        {
                            type: "ActionSet",
                            actions: [
                                {
                                    type: "Action.ShowCard",
                                    title: "ShowEmails",
                                    card: {
                                        type: "AdaptiveCard",
                                        body: [
                                            {
                                                type: "Input.Text",
                                                id: `copyEmails_${idx}`,  // ユニークなIDにするためにインデックスを付与
                                                value: userEmails,
                                                isMultiline: true,
                                                style: "text"
                                            }
                                        ],
                                        $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
                                        version: "1.4"
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        });
    });

    const adaptiveCardJson: string = JSON.stringify(card, null, 2);
    console.log(adaptiveCardJson);
    return adaptiveCardJson;
}
```

## Actions

```json
{"nodeId":"Scope-copy","serializedValue":{"type":"Scope","actions":{"Reactions_取得（Emoticon）":{"type":"Scope","actions":{"ChannelMessage？":{"type":"If","expression":{"and":[{"not":{"equals":["@triggerBody()?['teamsFlowRunContext']?['channelData']?['team']","@null"]}}]},"actions":{"Get_message_details_InChannel":{"type":"OpenApiConnection","inputs":{"parameters":{"messageId":"@triggerBody()?['teamsFlowRunContext']?['messagePayload']?['id']","threadType":"channel","body/recipient/groupId":"@triggerOutputs()?['body/teamsFlowRunContext/channelData/team/aadGroupId']","body/recipient/channelId":"@triggerOutputs()?['body/teamsFlowRunContext/channelData/channel/id']","body/recipient/parentMessageId":"@triggerOutputs()?['body/teamsFlowRunContext/messagePayload/replyToId']"},"host":{"apiId":"/providers/Microsoft.PowerApps/apis/shared_teams","connection":"shared_teams","operationId":"GetMessageDetails"}},"metadata":{"operationMetadataId":"06210aad-f59f-4c39-aae8-8ab53571ea91"}},"Compose":{"type":"Compose","inputs":"@outputs('Get_message_details_InChannel')?['body/reactions']","runAfter":{"Get_message_details_InChannel":["Succeeded"]},"metadata":{"operationMetadataId":"52790c17-d868-4fd1-987e-34c1e138584a"}},"Set_variable":{"type":"SetVariable","inputs":{"name":"Reactions","value":"@outputs('Get_message_details_InChannel')?['body/reactions']"},"runAfter":{"Compose":["Succeeded"]},"metadata":{"operationMetadataId":"284935d6-bd58-44c4-9b4c-07827cad73b8"}},"Set_variable_Message":{"type":"SetVariable","inputs":{"name":"Message","value":{"Message":"@if(greater(length(outputs('Get_message_details_InChannel')?['body/body/plainTextContent']),100),\r\nsubstring(outputs('Get_message_details_InChannel')?['body/body/plainTextContent'], 0,10100),outputs('Get_message_details_InChannel')?['body/body/plainTextContent'])","LinkToMessage":"@outputs('Get_message_details_InChannel')?['body/messageLink']"}},"runAfter":{"Set_variable":["Succeeded"]}}},"else":{"actions":{"Get_message_details_InChat":{"type":"OpenApiConnection","inputs":{"parameters":{"messageId":"@triggerBody()?['teamsFlowRunContext']?['messagePayload']?['id']","threadType":"groupchat","body/recipient":"@triggerOutputs()?['body/teamsFlowRunContext/conversation/id']"},"host":{"apiId":"/providers/Microsoft.PowerApps/apis/shared_teams","connection":"shared_teams","operationId":"GetMessageDetails"}},"metadata":{"operationMetadataId":"9dd45887-faf7-4325-8992-b04901a414e3"}},"Set_variable_2":{"type":"SetVariable","inputs":{"name":"Reactions","value":"@outputs('Get_message_details_InChat')?['body/reactions']"},"runAfter":{"Get_message_details_InChat":["Succeeded"]},"metadata":{"operationMetadataId":"ea211d03-6b6a-4620-b691-f35055cdaebe"}},"Set_variable_Message_in_Chat":{"type":"SetVariable","inputs":{"name":"Message","value":{"Message":"@if(greater(length(outputs('Get_message_details_InChat')?['body/body/plainTextContent']),100),\r\nsubstring(outputs('Get_message_details_InChat')?['body/body/plainTextContent'], 0,10100),outputs('Get_message_details_InChat')?['body/body/plainTextContent'])","LinkToMessage":"@outputs('Get_message_details_InChat')?['body/messageLink']"}},"runAfter":{"Set_variable_2":["Succeeded"]}}}},"metadata":{"operationMetadataId":"4004a979-97c8-45c6-9fb0-2d3788cff941"}}},"runAfter":{"Initialize_Users":["SUCCEEDED"]},"metadata":{"operationMetadataId":"cd7790be-db53-4b1b-a50b-f9c043d30fc3"}},"結果投稿":{"type":"Scope","actions":{"Condition":{"type":"If","expression":{"and":[{"not":{"equals":["@triggerBody()?['teamsFlowRunContext']?['channelData']['Team']","@null"]}}]},"actions":{"Reply_with_adaptive_card_in_a_channel":{"type":"OpenApiConnection","inputs":{"parameters":{"poster":"Flow bot","location":"Channel","body/parentMessageId":"@outputs('replyToId')","body/recipient/groupId":"@outputs('Get_message_details_InChannel')?['body/teamId']","body/recipient/channelId":"@outputs('Get_message_details_InChannel')?['body/channelId']","body/messageBody":"@outputs('Run_script')?['body/result']"},"host":{"apiId":"/providers/Microsoft.PowerApps/apis/shared_teams","connection":"shared_teams","operationId":"ReplyWithCardToConversation"}},"runAfter":{"replyToId":["Succeeded"]},"runtimeConfiguration":{"staticResult":{"staticResultOptions":"Disabled","name":"Reply_with_adaptive_card_in_a_channel0"}},"metadata":{"operationMetadataId":"8600d900-094f-4cc9-9294-aa5f59c17860"}},"replyToId":{"type":"Compose","description":"null: 親なので、Id, !null: 親Id","inputs":"@if(empty(outputs('Get_message_details_InChannel')?['body/replyToId']), outputs('Get_message_details_InChannel')?['body/Id'], outputs('Get_message_details_InChannel')?['body/replyToId'])","metadata":{"operationMetadataId":"085386cc-4f7d-4f4b-9866-61383cb5da3d"}}},"else":{"actions":{"Post_adaptive_card_in_a_chat_or_channel":{"type":"OpenApiConnection","inputs":{"parameters":{"poster":"Flow bot","location":"Group chat","body/recipient":"@outputs('Get_message_details_InChat')?['body/conversationId']","body/messageBody":"@outputs('Run_script')?['body/result']"},"host":{"apiId":"/providers/Microsoft.PowerApps/apis/shared_teams","connection":"shared_teams","operationId":"PostCardToConversation"}},"runtimeConfiguration":{"staticResult":{"staticResultOptions":"Disabled","name":"Post_adaptive_card_in_a_chat_or_channel0"}},"metadata":{"operationMetadataId":"f1fa9c6b-da04-4149-9896-8870b0f57b05"}}}},"metadata":{"operationMetadataId":"cc1202d5-e154-4abe-b279-982378592833"}}},"runAfter":{"Reactions_Analysis":["Succeeded"]},"metadata":{"operationMetadataId":"fcfd03b0-ae6e-4948-9bba-032adc98a8c9"}},"Reactions_Analysis":{"type":"Scope","actions":{"Select_ReactionsValues":{"type":"Select","inputs":{"from":"@variables('Reactions')","select":{"reactionType":"@item()?['reactionType']","userId":"@item()?['user/user/id']","reactionName":"@item()?['displayName']"}}},"Select_UserIDs":{"type":"Select","inputs":{"from":"@variables('Reactions')","select":"@item()?['user/user/id']"}},"For_each":{"type":"Foreach","foreach":"@outputs('Select_UserIDs')['body']","actions":{"Get_user_profile_(V2)":{"type":"OpenApiConnection","inputs":{"parameters":{"id":"@item()","$select":"displayName, mail"},"host":{"apiId":"/providers/Microsoft.PowerApps/apis/shared_office365users","connection":"shared_office365users","operationId":"UserProfile_V2"}}},"Append_to_array_variable":{"type":"AppendToArrayVariable","inputs":{"name":"Users","value":{"UserID":"@item()","DisplayName":"@outputs('Get_user_profile_(V2)')?['body/displayName']","EMail":"@outputs('Get_user_profile_(V2)')?['body/mail']"}},"runAfter":{"Get_user_profile_(V2)":["Succeeded"]}}},"runAfter":{"Select_UserIDs":["Succeeded"]}},"Compose_results":{"type":"Compose","inputs":"@variables('Users')","runAfter":{"For_each":["Succeeded"]}},"Run_script":{"type":"OpenApiConnection","inputs":{"parameters":{"source":"me","drive":"b!QSv_CUULHECjjx05iSyK255pBaQe5FlKprvq9VYwcdpPYNdo1e42Qa5Y147BwZtO","file":"01LEKUELAKOLXHO6LZH5EKLPFL4MC4J6A6","scriptId":"ms-officescript%3A%2F%2Fonedrive_business_itemlink%2F01LEKUELFQ54OHLZDOBNEIA665CL3KGMHD","ScriptParameters/reactionsString":"@string(body('Select_ReactionsValues'))","ScriptParameters/usersString":"@variables('Users')","ScriptParameters/originalMessageJSON":"@variables('Message')"},"host":{"apiId":"/providers/Microsoft.PowerApps/apis/shared_excelonlinebusiness","connection":"shared_excelonlinebusiness","operationId":"RunScriptProd"}},"runAfter":{"Select_ReactionsValues":["Succeeded"],"Compose_results":["Succeeded"]},"metadata":{"01U75VYIQNP6GOQZUV6VE3YORLAODZIEXC":"/DummyForOfficeScripts.xlsx","tableId":null,"01LEKUELAKOLXHO6LZH5EKLPFL4MC4J6A6":"/DummyForOfficeScript.xlsx"}}},"runAfter":{"Reactions_取得（Emoticon）":["Succeeded"]}},"Initialize_Users":{"type":"InitializeVariable","inputs":{"variables":[{"name":"Users","type":"array"}]},"runAfter":{"Initialize_Message":["SUCCEEDED"]}},"Initialize_Message":{"type":"InitializeVariable","inputs":{"variables":[{"name":"Message","type":"object"}]},"runAfter":{"Initialize_Reactions":["SUCCEEDED"]}},"Initialize_Reactions":{"type":"InitializeVariable","inputs":{"variables":[{"name":"Reactions","type":"array"}]},"metadata":{"operationMetadataId":"d196fb6a-df4e-41cf-a341-82c39403f450"}}},"runAfter":{},"metadata":{"operationMetadataId":"f3810333-05bf-4a43-a6e2-8323a7efcd24"}},"allConnectionData":{"Get_message_details_InChannel":{"connectionReference":{"api":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams"},"connection":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams/connections/shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"connectionName":"shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"referenceKey":"shared_teams"},"Get_message_details_InChat":{"connectionReference":{"api":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams"},"connection":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams/connections/shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"connectionName":"shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"referenceKey":"shared_teams"},"Reply_with_adaptive_card_in_a_channel":{"connectionReference":{"api":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams"},"connection":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams/connections/shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"connectionName":"shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"referenceKey":"shared_teams"},"Post_adaptive_card_in_a_chat_or_channel":{"connectionReference":{"api":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams"},"connection":{"id":"/providers/Microsoft.PowerApps/apis/shared_teams/connections/shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"connectionName":"shared-teams-c9efbf63-e5a2-4102-a864-e913ec0c8ef8"},"referenceKey":"shared_teams"},"Get_user_profile_(V2)":{"connectionReference":{"api":{"id":"/providers/Microsoft.PowerApps/apis/shared_office365users"},"connection":{"id":"/providers/Microsoft.PowerApps/apis/shared_office365users/connections/shared-office365user-8adbcca0-19c0-4a73-8fdb-225b07b9092b"},"connectionName":"shared-office365user-8adbcca0-19c0-4a73-8fdb-225b07b9092b"},"referenceKey":"shared_office365users"},"Run_script":{"connectionReference":{"api":{"id":"/providers/Microsoft.PowerApps/apis/shared_excelonlinebusiness"},"connection":{"id":"/providers/Microsoft.PowerApps/apis/shared_excelonlinebusiness/connections/shared-excelonlinebu-499d4373-d0fb-4e25-9c76-65a0ff2c5a73"},"connectionName":"shared-excelonlinebu-499d4373-d0fb-4e25-9c76-65a0ff2c5a73"},"referenceKey":"shared_excelonlinebusiness"}},"staticResults":{"Reply_with_adaptive_card_in_a_channel":{"status":"Succeeded","outputs":{"statusCode":"OK"}},"Post_adaptive_card_in_a_chat_or_channel":{"status":"Succeeded","outputs":{"statusCode":"OK"}}},"isScopeNode":true,"mslaNode":true}
```