0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-25

背景

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

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

結論

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

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

具体的なイメージは?

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

image.png

Adaptive Card とは?

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

あとがき

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


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

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

動作イメージ

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

image.png

こんな感じで、リアクションの集計をする。
Email のところをクリックして emails list をコピーすれば、以下のようなことができる

  • 特定のリアクションをした人だけの会議招集や、チャットなどに使える
  • Polls を使わずに、簡単なリアクション集計

image.png

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

image.png

For a selected message

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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?