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?

【やってみよう】Copilot Studio × Azure DevOps で「自社専用AIコードレビューエージェント」を作る【後編】

0
Last updated at Posted at 2026-10-03

前編はこちら

💡 この記事のポイント

  • 【前編】の課題だった曖昧な指示に起因する 404 エラーを大幅に改善!
  • Copilot Studioの「トピック」と「アダプティブカード」を駆使して、チャット内にリッチな選択UI(ドロップダウン)を実装!
  • プロジェクト名やブランチ名の手入力・コピペ作業をゼロにし、タイポ知らずの爆速・快適レビュー体験を実現!

はじめに:前編のおさらいと「404エラー」の壁

こんにちは!【前編】の記事では、社内に眠っていた Microsoft Copilot Studio のライセンスと Azure DevOps の標準コネクタを組み合わせて、「自社のコーディング規約(.md)を熟知したAIコードレビューエージェント」 をお手軽に作る方法を解説しました。

プロンプトと標準ツールだけでも驚くほど実用的なレビューをしてくれるようになったのですが、実際に現場で使ってみると 「ある課題」 が浮き彫りになりました。

それは、ユーザーがチャットで 「〇〇についてレビューして」とざっくりした指示を投げたときに発生する 404 HTTP エラー(ConnectorRequestFailure)問題 です。
ConnectorRequestFailure

エージェント側が曖昧なチャットからプロジェクト名やリポジトリ名を推測してAPIの引数にセットしてしまうため、実在しない名前を指定してしまいエラーで止まってしまうのです。かといって、毎回人間が一字一句正確なプロジェクト名やリポジトリ名、長いブランチ名を調べてコピペするのは本末転倒ですよね。

「チャットの中で、プロジェクトやブランチをプルダウンからポチポチ選ぶだけでレビューできたら最高なのに……!」

そこで本記事(後編)では、Copilot Studioの 「トピック」 と 「アダプティブカード(Adaptive Cards)」 を駆使して、Azure DevOpsからリアルタイムに一覧を取得し、 UI上で確実にレビュー対象を指定できる超快適なエージェントへの進化方法 を徹底解説します!


解決のアプローチ:「トピック」×「アダプティブカード」で選択UIを作る!

今回の改善のキモは、AIの自由な推測(オーケストレーション)にすべてを任せるのではなく、「対象を特定する部分」をCopilot Studioの「トピック(会話フロー)」と「アダプティブカード」でカチッと制御してあげることです。

具体的には、以下のような流れを作ります。

  1. ユーザーがレビューを依頼するとトピックが起動
  2. エージェントが裏側で 「プロジェクト一覧」 をAPI取得し、ドロップダウンUIで提示
  3. ユーザーがプロジェクトを選ぶと、それに紐づく 「リポジトリ一覧」「ブランチ一覧」 をAPI取得
  4. 2つ目のカードUIで、リポジトリ・ブランチ・レビュー範囲(特定PR / ブランチ差分 / 特定ファイル / フォルダ全体) を選択・入力
  5. 確定した正確な情報をもとにソースコードを取得し、AIレビューを実行!

これなら、ユーザーのタイポもAIの推測ミスも物理的に発生しません!

トピックの全体フローチャート

今回作成するトピックの全体像は以下の通りです。

Mermaidコード
flowchart TD
    START("トピック開始")
    MSG["メッセージ: レビュー条件を入力してください"]
    TOOL1["ツール: プロジェクト一覧取得"]
    CARD1["カード①: 対象プロジェクトを選択"]
    TOOL2["ツール: リポジトリ一覧取得"]
    TOOL3["ツール: ブランチ一覧取得"]
    CARD2["カード②: リポジトリ・ブランチ・レビュー範囲・対象を入力"]
    ACT{"押されたボタン"}
    INIT["JsonData を空文字で初期化"]
    TYPE{"レビュー範囲"}
    CONFIRM["メッセージ: レビュー条件の確認"]
    AI["RecognizeIntent: 条件と取得データを渡してAIレビュー"]
    CUSTOM["メッセージ: 具体的な指示を入力してください"]
    FIN1["トピック終了"]
    FIN2["トピック終了"]

    START --> MSG --> TOOL1 --> CARD1 --> TOOL2 --> TOOL3 --> CARD2 --> ACT
    ACT --> |"レビューを開始"| CONFIRM --> INIT --> TYPE
    ACT --> |"別の指示をする"| CUSTOM --> FIN2
    AI --> FIN1

    subgraph SG_FILE ["特定ファイル"]
        F1["GET items: 対象ファイルの内容"]
        F2["JsonData に格納"]
        F1 --> F2
    end

    subgraph SG_PR ["特定PR"]
        P1["GET pullRequests: PR情報を取得"]
        P2["ParseValue: マージ元とマージ先を取得"]
        P3["GET diffs/commits: 変更ファイル一覧"]
        P4["Filter: 削除以外のファイルを抽出"]
        P5{"Foreach: 変更ファイルごと"}
        P6["GET items: マージ元のファイル内容"]
        P7["JsonData に追記"]
        P1 --> P2 --> P3 --> P4 --> P5
        P5 --> |"次のファイル"| P6 --> P7 --> P5
    end

    subgraph SG_BRANCH ["ブランチ間差分"]
        B1["GET diffs/commits: 比較元と対象の差分"]
        B2["Filter: 削除以外のファイルを抽出"]
        B3{"Foreach: 変更ファイルごと"}
        B4["GET items: 対象ブランチのファイル内容"]
        B5["JsonData に追記"]
        B1 --> B2 --> B3
        B3 --> |"次のファイル"| B4 --> B5 --> B3
    end

    subgraph SG_FOLDER ["特定フォルダ配下全体"]
        D1["GET items: フォルダ配下を再帰取得"]
        D2["Filter: ファイルのみ抽出"]
        D3{"Foreach: フォルダ内ファイルごと"}
        D4["GET items: 対象ブランチのファイル内容"]
        D5["JsonData に追記"]
        D1 --> D2 --> D3
        D3 --> |"次のファイル"| D4 --> D5 --> D3
    end

    TYPE --> |"特定ファイル"| F1
    TYPE --> |"特定PR"| P1
    TYPE --> |"ブランチ間差分"| B1
    TYPE --> |"特定フォルダ配下全体"| D1

    F2 --> |"完了"| AI
    P5 --> |"完了"| AI
    B3 --> |"完了"| AI
    D3 --> |"完了"| AI

    classDef api fill:#e3f2fd,stroke:#1e88e5,color:#0d47a1
    classDef card fill:#fff8e1,stroke:#f9a825,color:#5d4037
    classDef ai fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
    classDef loop fill:#e8f5e9,stroke:#43a047,color:#1b5e20

    class TOOL1,TOOL2,TOOL3,F1,P1,P3,P6,B1,B4,D1,D4 api
    class CARD1,CARD2 card
    class P5,B3,D3 loop
    class AI ai

本記事(後編)のゴールと作り方の流れ

後編では、以下の3ステップでエージェントにUI選択式のトピックを組み込んでいきます。

  1. トピックの新規作成
  2. トピックのロジック設定 👈 アダプティブカードと分岐処理のキモ!
    • Step 2-a: Azure DevOpsプロジェクト一覧の取得と選択UI
    • Step 2-b: リポジトリ&ブランチ一覧の取得と詳細条件の入力UI
    • Step 2-c: レビュー範囲に応じたデータ取得とAIレビューへの引き継ぎ
  3. トピックのテストと動作確認

それでは、さっそく作っていきましょう!


1. トピックを作成

まずは、Copilot Studioの上部メニューから**「トピック」**タブを開きます。
「+ トピックを追加」から新しいカスタムトピックを作成し、ユーザーが「レビューして」「コードを見て」などと話しかけた際にこのトピックが起動するようにトリガーを設定します。

トピック一覧画面

今回は「レビュー条件の指定」をトリガーとします。
トリガーノード

2. トピックのロジック設定

ここが後編のメインパートです!フローチャートに沿って、順番にノードを組み立てていきます。

Step 2-a. Azure DevOpsプロジェクト一覧から選択させる(カード①)

まずは、ユーザーに対象のプロジェクトを選んでもらうための処理を作ります。

1. メッセージノードの追加

  • 「レビュー条件を入力してください」と案内を表示します

2. ツール(コネクタ)でプロジェクト一覧を取得

  • アクションから前編で作成したツールの「プロジェクトの一覧表示」を呼び出し、組織内のプロジェクト一覧データを変数に格納します
    ツール
  • 出力の変数名は ListGitProjectsとします
    出力の変数名

3. アダプティブカードでドロップダウンUIを表示(カード①)

  • 「アダプティブカードで質問する」ノードを追加します
    アダプティブカードで質問する

  • プロパティから形式を「式」に変更し、下記の式を入力します。先ほど取得したプロジェクト一覧の配列データをバインドし、ドロップダウンリスト(Input.ChoiceSet) として画面に表示します
    式カード

AdaptiveCard 式
{
  type: "AdaptiveCard",
  '$schema': "http://adaptivecards.io/schemas/adaptive-card.json",
  version: "1.4",
  body: [
    {
      type: "TextBlock",
      text: "📝 対象プロジェクトの指定",
      weight: "Bolder",
      size: "Medium"
    },
    {
      type: "Input.ChoiceSet",
      id: "SelectedProject",
      label: "対象プロジェクト",
      style: "filtered",
      isMultiSelect: false,
      isRequired: true,
      errorMessage: "プロジェクトを選択してください。",
      placeholder: "プロジェクトを検索・選択...",
      choices: ForAll(
        Topic.ListGitProjects,
        {
          title: Text(ThisRecord.name),
          value: Text(ThisRecord.name)
        }
      )
    }
  ],
  actions: [
    {
      type: "Action.Submit",
      title: "レビュー条件を設定"
    }
  ]
}

4. 出力変数名を設定
変数名を「SelectedProject」とし、どのトピックでも扱えるように、グローバルスコープに設定します。
出力変数名

これで、最初のステップである「プロジェクト名の確実な特定」ができました!

Step 2-b. Gitリポジトリとブランチの一覧から選択させる(カード②)

プロジェクトが決まったら、そのプロジェクトの中にある「リポジトリ」と「ブランチ」の一覧を取得して、さらに詳細なレビュー条件を入力してもらいます。

1. リポジトリ一覧とブランチ一覧の取得

  • カード①で選択された「プロジェクト名」を引数に渡して、ツールで 「リポジトリ一覧」 と 「ブランチ一覧」 を連続で取得します
  • 出力変数名を「ListGitRepositories」と「ListGitBranches」に設定します
    リポジトリ一覧とブランチ一覧の取得

2. 詳細条件を入力するアダプティブカードの表示(カード②)

  • 2つ目のアダプティブカードノードを配置し、以下の入力項目を1つのカード内にまとめて表示します
    • 対象リポジトリ:取得したリポジトリ一覧のドロップダウン
    • 対象ブランチ(および比較元ブランチ):取得したブランチ一覧のドロップダウン
    • レビュー範囲の選択:「特定PR」「ブランチ間差分」「特定ファイル」「特定フォルダ配下全体」から選択するドロップダウン
    • 付加情報のテキスト入力ボックス:PRのIDや、特定ファイル/フォルダのパスを入力するテキストボックス(Input.Text)
    • アクションボタン:「レビューを開始」ボタンと、レビュー以外の質問に切り替えられる「別の指示をする」ボタンの2つを設置

アダプティブカード

AdaptiveCard 式
{
  type: "AdaptiveCard",
  '$schema': "http://adaptivecards.io/schemas/adaptive-card.json",
  version: "1.4",
  body: [
    {
      type: "TextBlock",
      text: "📝 レビュー条件の入力",
      weight: "Bolder",
      size: "Medium"
    },
    {
      type: "TextBlock",
      text: "レビューに必要な情報を選択・入力してください。",
      isSubtle: true,
      wrap: true,
      spacing: "None"
    },
    {
      type: "Input.ChoiceSet",
      id: "SelectedRepo",
      label: "1. 対象リポジトリ",
      style: "filtered",
      isMultiSelect: false,
      isRequired: true,
      errorMessage: "リポジトリを選択してください。",
      placeholder: "リポジトリを検索・選択...",
      choices: ForAll(
        Topic.ListGitRepositories,
        {
          title: Text(ThisRecord.name),
          value: Text(ThisRecord.name)
        }
      )
    },
    {
      type: "Input.ChoiceSet",
      id: "SelectedBranch",
      label: "2. 対象ブランチ",
      style: "filtered",
      isMultiSelect: false,
      isRequired: true,
      errorMessage: "ブランチを選択してください。",
      placeholder: "ブランチを検索・選択...",
      choices: ForAll(
        Topic.ListGitBranches,
        {
          title: Substitute(Text(ThisRecord.name), "refs/heads/", ""),
          value: Substitute(Text(ThisRecord.name), "refs/heads/", "")
        }
      )
    },
    {
      type: "Input.ChoiceSet",
      id: "ReviewType",
      label: "3. レビュー範囲の粒度",
      style: "compact",
      isMultiSelect: false,
      isRequired: true,
      errorMessage: "レビュー範囲を選択してください。",
      value: "特定PR",
      choices: [
        {
          title: "特定PR(プルリクエスト)",
          value: "特定PR"
        },
        {
          title: "ブランチ間差分",
          value: "ブランチ間差分"
        },
        {
          title: "特定ファイル",
          value: "特定ファイル"
        },
        {
          title: "特定フォルダ配下全体",
          value: "特定フォルダ配下全体"
        }
      ]
    },
    {
      type: "Input.Text",
      id: "TargetID",
      label: "4. 対象パス / PR番号 / 比較元ブランチ",
      placeholder: "例: 123 (PR番号) や /src/app/app.component.ts",
      isRequired: true,
      errorMessage: "対象のPR番号やファイルパスを入力してください。"
    }
  ],
  actions: [
    {
      type: "Action.Submit",
      title: "レビューを開始",
      data: {
        ActionType: "Review"
      }
    },
    {
      type: "Action.Submit",
      title: "別の指示をする",
      associatedInputs: "none",
      data: {
        ActionType: "Custom"
      }
    }
  ]
}
  • 出力変数の設定を行います。
    • トピックスコープ
      ActionType
    • グローバルスコープ
      ReviewType
      SelectedBranch
      SelectedRepo
      TargetID

このように1枚のカードにギュッと凝縮することで、ユーザーは画面上のUIを上から順にポチポチ選ぶだけで、迷わず条件をセットできるようになります!

Step 2-c. 選択された条件に応じたデータ取得とAIレビューの実行

カード②のボタンが押された後の分岐処理を作成します。

① 押されたボタンによる分岐

  • 「別の指示をする」が押された場合:
    • 「具体的な指示を入力してください」とメッセージを出してトピックを終了し、自由なチャットへ移行できるようにします
  • 「レビューを開始」が押された場合:
    • 選択されたレビュー条件の確認メッセージを表示し、ソースコード格納用の変数(JsonData)を空文字で初期化して、次の「レビュー範囲」の分岐へ進みます

レビュー条件の確認メッセージを

ボタンによる分岐

② レビュー範囲に応じたソースコードの取得(4パターン)

カード②で選ばれた「レビュー範囲」によって、Azure DevOpsにHTTP要求を送信してソースコード(JSON)を取得し、変数 JsonData に格納・追記していきます。

ソースコードの取得

  • パターンA:特定ファイル
    • GET items で対象ファイルの内容を取得し、JsonData に格納します
  • パターンB:特定PR(プルリクエスト)
    • GET pullRequests でPR情報を取得し、「解析(ParseValue)」ノードでマージ元とマージ先のブランチを抽出します
    • GET diffs/commits で変更ファイル一覧を取得し、削除されたファイル以外をFilterで抽出します(前編で解説した404エラー対策です!)

    ⚠️ 注意:
    変更ファイル数の上限について 本記事のトピックで使っている diffs/commits API は、1回の呼び出しで返す変更件数が既定で 100件 までです。
    この件数にはフォルダも含まれます。変更ファイルが多いPRやブランチ間差分では、一部のファイルがレビュー対象から漏れる可能性があります。
    大規模な変更をレビューする場合は、対象を分割する(特定ファイル/特定フォルダ指定を併用する)か、$top と $skip を使ったページング処理を追加してください。

    • Foreach ループで変更ファイルごとに GET items を回し、マージ元のファイル内容を JsonData に追記していきます
  • パターンC:ブランチ間差分
    • GET diffs/commits で比較元と対象ブランチの差分を取得します
    • 削除以外のファイルをFilterで抽出し、Foreach ループで各ファイルの内容を GET items で取得して JsonData に追記します
  • パターンD:特定フォルダ配下全体
    • GET items(再帰取得)でフォルダ配下のアイテム一覧を取得し、フォルダを除いた「ファイルのみ」をFilterで抽出します
    • Foreach ループで各ファイルの内容を GET items で取得して JsonData に追記します

ソースコードの取得深掘り

トピック全体のコード ※そのままコピーしても接続エラーになります。ツールやコネクタのノードを適宜修正いただけたらと思います。

コードエディターは画面右上のこちらのメニューから開けます。

コードエディター


kind: AdaptiveDialog
modelDescription: 「レビュー条件の指定」を行う旨チャットにある場合と、レビュー条件が不明瞭の場合に実行し、レビュー条件を指定させる
beginDialog:
  kind: OnRecognizedIntent
  id: main
  intent: {}
  actions:
    - kind: SendActivity
      id: sendMessage_M0LuhV
      activity:
        text:
          - レビュー条件を入力してください。
        speak:
          - "{System.Bot.Name} にお電話いただき、ありがとうございます。一部の応答が AI によって生成されているため、正確性の確認が必要な場合があります。お問い合わせ内容をお知らせください。"

    - kind: BeginDialog
      id: ZdM6Zh
      input: {}
      dialog: new_kaneoka_nr_blog.action.AzureDevOps-
      output:
        binding:
          value: Topic.ListGitProjects

    - kind: AdaptiveCardPrompt
      id: hQRxeP
      card: |-
        ={
          type: "AdaptiveCard",
          '$schema': "http://adaptivecards.io/schemas/adaptive-card.json",
          version: "1.4",
          body: [
            {
              type: "TextBlock",
              text: "📝 対象プロジェクトの指定",
              weight: "Bolder",
              size: "Medium"
            },
            {
              type: "Input.ChoiceSet",
              id: "SelectedProject",
              label: "対象プロジェクト",
              style: "filtered",
              isMultiSelect: false,
              isRequired: true,
              errorMessage: "プロジェクトを選択してください。",
              placeholder: "プロジェクトを検索・選択...",
              choices: ForAll(
                Topic.ListGitProjects,
                {
                  title: Text(ThisRecord.name),
                  value: Text(ThisRecord.name)
                }
              )
            }
          ],
          actions: [
            {
              type: "Action.Submit",
              title: "レビュー条件を設定"
            }
          ]
        }
      output:
        binding:
          SelectedProject: Global.SelectedProject

      outputType:
        properties:
          SelectedProject: String

    - kind: BeginDialog
      id: QThu69
      input:
        binding:
          project: =Global.SelectedProject

      dialog: new_kaneoka_nr_blog.action.AzureDevOps-Git
      output:
        binding:
          value: Topic.ListGitRepositories

    - kind: BeginDialog
      id: tYtxbP
      input:
        binding:
          project: =Global.SelectedProject

      dialog: new_kaneoka_nr_blog.action.AzureDevOps-Git_0V7
      output:
        binding:
          value: Topic.ListGitBranches

    - kind: AdaptiveCardPrompt
      id: Tw6Mhs
      card: |-
        ={
          type: "AdaptiveCard",
          '$schema': "http://adaptivecards.io/schemas/adaptive-card.json",
          version: "1.4",
          body: [
            {
              type: "TextBlock",
              text: "📝 レビュー条件の入力",
              weight: "Bolder",
              size: "Medium"
            },
            {
              type: "TextBlock",
              text: "レビューに必要な情報を選択・入力してください。",
              isSubtle: true,
              wrap: true,
              spacing: "None"
            },
            {
              type: "Input.ChoiceSet",
              id: "SelectedRepo",
              label: "1. 対象リポジトリ",
              style: "filtered",
              isMultiSelect: false,
              isRequired: true,
              errorMessage: "リポジトリを選択してください。",
              placeholder: "リポジトリを検索・選択...",
              choices: ForAll(
                Topic.ListGitRepositories,
                {
                  title: Text(ThisRecord.name),
                  value: Text(ThisRecord.name)
                }
              )
            },
            {
              type: "Input.ChoiceSet",
              id: "SelectedBranch",
              label: "2. 対象ブランチ",
              style: "filtered",
              isMultiSelect: false,
              isRequired: true,
              errorMessage: "ブランチを選択してください。",
              placeholder: "ブランチを検索・選択...",
              choices: ForAll(
                Topic.ListGitBranches,
                {
                  title: Substitute(Text(ThisRecord.name), "refs/heads/", ""),
                  value: Substitute(Text(ThisRecord.name), "refs/heads/", "")
                }
              )
            },
            {
              type: "Input.ChoiceSet",
              id: "ReviewType",
              label: "3. レビュー範囲の粒度",
              style: "compact",
              isMultiSelect: false,
              isRequired: true,
              errorMessage: "レビュー範囲を選択してください。",
              value: "特定PR",
              choices: [
                {
                  title: "特定PR(プルリクエスト)",
                  value: "特定PR"
                },
                {
                  title: "ブランチ間差分",
                  value: "ブランチ間差分"
                },
                {
                  title: "特定ファイル",
                  value: "特定ファイル"
                },
                {
                  title: "特定フォルダ配下全体",
                  value: "特定フォルダ配下全体"
                }
              ]
            },
            {
              type: "Input.Text",
              id: "TargetID",
              label: "4. 対象パス / PR番号 / 比較元ブランチ",
              placeholder: "例: 123 (PR番号) や /src/app/app.component.ts",
              isRequired: true,
              errorMessage: "対象のPR番号やファイルパスを入力してください。"
            }
          ],
          actions: [
            {
              type: "Action.Submit",
              title: "レビューを開始",
              data: {
                ActionType: "Review"
              }
            },
            {
              type: "Action.Submit",
              title: "別の指示をする",
              associatedInputs: "none",
              data: {
                ActionType: "Custom"
              }
            }
          ]
        }
      output:
        binding:
          ActionType: Topic.ActionType
          ReviewType: Global.ReviewType
          SelectedBranch: Global.SelectedBranch
          SelectedRepo: Global.SelectedRepo
          TargetID: Global.TargetID

      outputType:
        properties:
          ActionType: String
          ReviewType: String
          SelectedBranch: String
          SelectedRepo: String
          TargetID: String

    - kind: SendActivity
      id: sendActivity_iJ861J
      activity: |-
        以下の条件でコードを取得し、レビューを開始します。
        - プロジェクト:{Global.SelectedProject}
        - リポジトリ:{Global.SelectedRepo}
        - ブランチ:{Global.SelectedBranch}
        - レビュー範囲:{Global.ReviewType}
        - 対象パス/PR番号/比較元ブランチ:{Global.TargetID}

    - kind: ConditionGroup
      id: conditionGroup_7XzzgV
      conditions:
        - id: conditionItem_rpWPag
          condition: =Topic.ActionType = "Review"
          actions:
            - kind: SetVariable
              id: setVariable_EXBC50
              variable: Topic.JsonData
              value: =""

            - kind: ConditionGroup
              id: conditionGroup_zfVFxo
              conditions:
                - id: conditionItem_IgmOwk
                  condition: =Global.ReviewType = "特定ファイル"
                  actions:
                    - kind: InvokeConnectorAction
                      id: invokeConnectorAction_ZEmLpX
                      input:
                        binding:
                          account: NextRead
                          Method: GET
                          Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/items?path=" & EncodeUrl(Global.TargetID) & "&versionDescriptor.version=" & EncodeUrl(Global.SelectedBranch) & "&versionDescriptor.versionType=branch&includeContent=true&$format=json&api-version=7.1"

                      output:
                        kind: SingleVariableOutputBinding
                        variable: Topic.HttpRequest1

                      connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                      connectionProperties:
                        name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                        mode: Invoker

                      operationId: HttpRequest

                    - kind: SetVariable
                      id: setVariable_Wzpqbe
                      variable: Topic.JsonData
                      value: =JSON(Topic.HttpRequest1)

                - id: conditionItem_GVk86d
                  condition: =Global.ReviewType = "特定PR"
                  actions:
                    - kind: InvokeConnectorAction
                      id: invokeConnectorAction_zew5Xy
                      input:
                        binding:
                          account: NextRead
                          Method: GET
                          Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/pullRequests/" & Global.TargetID & "?api-version=7.1"

                      output:
                        kind: SingleVariableOutputBinding
                        variable: Topic.HttpRequest2

                      connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                      connectionProperties:
                        name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                        mode: Invoker

                      operationId: HttpRequest

                    - kind: ParseValue
                      id: parseValue_PRInfo
                      variable: Topic.ParsedPRInfo
                      valueType:
                        kind: Record
                        properties:
                          sourceRefName: String
                          targetRefName: String

                      value: =Topic.HttpRequest2

                    - kind: InvokeConnectorAction
                      id: invokeConnectorAction_PRDiff
                      input:
                        binding:
                          account: NextRead
                          Method: GET
                          Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/diffs/commits?baseVersion=" & EncodeUrl(Substitute(Topic.ParsedPRInfo.targetRefName, "refs/heads/", "")) & "&baseVersionType=branch&targetVersion=" & EncodeUrl(Substitute(Topic.ParsedPRInfo.sourceRefName, "refs/heads/", "")) & "&targetVersionType=branch&api-version=7.1"

                      output:
                        kind: SingleVariableOutputBinding
                        variable: Topic.Res_PRDiff

                      connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                      connectionProperties:
                        name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                        mode: Invoker

                      operationId: HttpRequest

                    - kind: ParseValue
                      id: parseValue_PRDiff
                      variable: Topic.ParsedPRDiff
                      valueType:
                        kind: Record
                        properties:
                          changes:
                            type:
                              kind: Table
                              properties:
                                changeType: String
                                item:
                                  type:
                                    kind: Record
                                    properties:
                                      gitObjectType: String
                                      path: String

                      value: =Topic.Res_PRDiff

                    - kind: SetVariable
                      id: setVariable_PRFiles
                      variable: Topic.PRChangedFiles
                      value: =Filter(Topic.ParsedPRDiff.changes, ThisRecord.item.gitObjectType = "blob" && ThisRecord.changeType <> "delete")

                    - kind: Foreach
                      id: foreach_PRFiles
                      items: =Topic.PRChangedFiles
                      value: Topic.LoopPRFile
                      actions:
                        - kind: InvokeConnectorAction
                          id: invokeConnectorAction_PRFileContent
                          input:
                            binding:
                              account: NextRead
                              Method: GET
                              Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/items?path=" & EncodeUrl(Topic.LoopPRFile.item.path) & "&versionDescriptor.version=" & EncodeUrl(Substitute(Topic.ParsedPRInfo.sourceRefName, "refs/heads/", "")) & "&versionDescriptor.versionType=branch&includeContent=true&$format=json&api-version=7.1"

                          output:
                            kind: SingleVariableOutputBinding
                            variable: Topic.Res_PRFileContent

                          connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                          connectionProperties:
                            name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                            mode: Invoker

                          operationId: HttpRequest

                        - kind: SetVariable
                          id: setVariable_AppendPRJson
                          variable: Topic.JsonData
                          value: "=Topic.JsonData & Char(10) & \"=== File: \" & Topic.LoopPRFile.item.path & \" (\" & Topic.LoopPRFile.changeType & \") ===\" & Char(10) & JSON(Topic.Res_PRFileContent)"

                - id: conditionItem_tmvOD7
                  condition: =Global.ReviewType = "ブランチ間差分"
                  actions:
                    - kind: InvokeConnectorAction
                      id: invokeConnectorAction_YLLLcy
                      input:
                        binding:
                          account: NextRead
                          Method: GET
                          Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/diffs/commits?baseVersion=" & EncodeUrl(Global.TargetID) & "&baseVersionType=branch&targetVersion=" & EncodeUrl(Global.SelectedBranch) & "&targetVersionType=branch&api-version=7.1"

                      output:
                        kind: SingleVariableOutputBinding
                        variable: Topic.HttpRequest3

                      connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                      connectionProperties:
                        name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                        mode: Invoker

                      operationId: HttpRequest

                    - kind: ParseValue
                      id: parseValue_BranchDiff
                      variable: Topic.ParsedBranchDiff
                      valueType:
                        kind: Record
                        properties:
                          changes:
                            type:
                              kind: Table
                              properties:
                                changeType: String
                                item:
                                  type:
                                    kind: Record
                                    properties:
                                      gitObjectType: String
                                      path: String

                      value: =Topic.HttpRequest3

                    - kind: SetVariable
                      id: setVariable_BranchFiles
                      variable: Topic.BranchChangedFiles
                      value: =Filter(Topic.ParsedBranchDiff.changes, ThisRecord.item.gitObjectType = "blob" && ThisRecord.changeType <> "delete")

                    - kind: Foreach
                      id: foreach_BranchFiles
                      items: =Topic.BranchChangedFiles
                      value: Topic.LoopBranchFile
                      actions:
                        - kind: InvokeConnectorAction
                          id: invokeConnectorAction_BranchFileContent
                          input:
                            binding:
                              account: NextRead
                              Method: GET
                              Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/items?path=" & EncodeUrl(Topic.LoopBranchFile.item.path) & "&versionDescriptor.version=" & EncodeUrl(Global.SelectedBranch) & "&versionDescriptor.versionType=branch&includeContent=true&$format=json&api-version=7.1"

                          output:
                            kind: SingleVariableOutputBinding
                            variable: Topic.Res_BranchFileContent

                          connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                          connectionProperties:
                            name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                            mode: Invoker

                          operationId: HttpRequest

                        - kind: SetVariable
                          id: setVariable_AppendBranchJson
                          variable: Topic.JsonData
                          value: "=Topic.JsonData & Char(10) & \"=== File: \" & Topic.LoopBranchFile.item.path & \" (\" & Topic.LoopBranchFile.changeType & \") ===\" & Char(10) & JSON(Topic.Res_BranchFileContent)"

                - id: conditionItem_DZqLla
                  condition: =Global.ReviewType = "特定フォルダ配下全体"
                  actions:
                    - kind: InvokeConnectorAction
                      id: invokeConnectorAction_QQJR5e
                      input:
                        binding:
                          account: NextRead
                          Method: GET
                          Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/items?scopePath=" & EncodeUrl(Global.TargetID) & "&recursionLevel=Full&versionDescriptor.version=" & EncodeUrl(Global.SelectedBranch) & "&versionDescriptor.versionType=branch&api-version=7.1"

                      output:
                        kind: SingleVariableOutputBinding
                        variable: Topic.HttpRequest4

                      connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                      connectionProperties:
                        name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                        mode: Invoker

                      operationId: HttpRequest

                    - kind: ParseValue
                      id: parseValue_FolderItems
                      variable: Topic.ParsedFolder
                      valueType:
                        kind: Record
                        properties:
                          value:
                            type:
                              kind: Table
                              properties:
                                gitObjectType: String
                                path: String

                      value: =Topic.HttpRequest4

                    - kind: SetVariable
                      id: setVariable_FolderFiles
                      variable: Topic.FolderFiles
                      value: =Filter(Topic.ParsedFolder.value, ThisRecord.gitObjectType = "blob")

                    - kind: Foreach
                      id: foreach_FolderFiles
                      items: =Topic.FolderFiles
                      value: Topic.LoopFolderFile
                      actions:
                        - kind: InvokeConnectorAction
                          id: invokeConnectorAction_FolderFileContent
                          input:
                            binding:
                              account: NextRead
                              Method: GET
                              Uri: =Global.SelectedProject & "/_apis/git/repositories/" & Global.SelectedRepo & "/items?path=" & EncodeUrl(Topic.LoopFolderFile.path) & "&versionDescriptor.version=" & EncodeUrl(Global.SelectedBranch) & "&versionDescriptor.versionType=branch&includeContent=true&$format=json&api-version=7.1"

                          output:
                            kind: SingleVariableOutputBinding
                            variable: Topic.Res_FolderFileContent

                          connectionReference: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                          connectionProperties:
                            name: cr677_devops_review.shared_visualstudioteamservices.2cad0ff0d692482592fdab2270ff3b4d
                            mode: Invoker

                          operationId: HttpRequest

                        - kind: SetVariable
                          id: setVariable_AppendFolderJson
                          variable: Topic.JsonData
                          value: "=Topic.JsonData & Char(10) & \"=== File: \" & Topic.LoopFolderFile.path & \" ===\" & Char(10) & JSON(Topic.Res_FolderFileContent)"

            - kind: RecognizeIntent
              id: Jcb5RC
              userInput: |-
                ="以下の取得済みJSONデータを解析し、指示に従ってコードレビュー結果を出力してください。" & Char(10) &
                "【対象プロジェクト】" & Global.SelectedProject & Char(10) &
                "【対象リポジトリ】" & Global.SelectedRepo & Char(10) &
                "【対象ブランチ】" & Global.SelectedBranch & Char(10) &
                "【レビュー粒度】" & Global.ReviewType & Char(10) &
                "【取得データ(JSON)】" & Char(10) & Topic.JsonData

      elseActions:
        - kind: SendActivity
          id: sendActivity_CustomPrompt
          activity: どのような処理をご希望ですか?具体的な指示を入力してください。

    - kind: EndDialog
      id: xtESB7

💡 ワンポイント:トピック側での深掘りするデータ取得は「任意」でもOK!
上記の②(4パターンのデータ取得ループ処理)は、トピック内で明示的に組むことで 「確実に・安定した手順でデータを取得できる」 という大きなメリットがありますが、実は 省略しても動作します!

カード①・②で「プロジェクト名」「リポジトリ名」「ブランチ名」「PR番号」などの正確な文字列さえ特定できていれば、あとはその条件テキストをそのままエージェントに渡すだけで、【前編】で設定したツールを使ってAI自身が同じように裏側でAPIを叩き、差分を取得してくれます。

「まずは手軽に動かしたい!」という方はカード②の直後にステップ③へ繋げ、「一般的な規模のPRであれば安定稼働させたい!」という方は上記のフローを組む、というように段階的に作り込むのがおすすめです!

③ 特定した情報をエージェントに認識させてレビュー実行!

データの取得(または条件の確定)が完了したら、最後に RecognizeIntent(意図の認識 / 生成応答) ノードへ繋ぎます。

意図の認識

"以下の取得済みJSONデータを解析し、指示に従ってコードレビュー結果を出力してください。" & Char(10) &
"【対象プロジェクト】" & Global.SelectedProject & Char(10) &
"【対象リポジトリ】" & Global.SelectedRepo & Char(10) &
"【対象ブランチ】" & Global.SelectedBranch & Char(10) &
"【レビュー粒度】" & Global.ReviewType & Char(10) &
"【取得データ(JSON)】" & Char(10) & Topic.JsonData

ここで、ユーザーが選択した条件と JsonData に格納されたソースコードデータをエージェントの頭脳(LLM)に引き渡すことで、【前編】で設定したシステムプロンプトと自社コーディング規約(ナレッジ)に基づいた高精度なAIコードレビューが一気に実行されます!

3. トピックのテスト

すべてのロジックが組めたら、画面右側のテストチャットで実際に動かしてみましょう!

チャット欄に「レビューして」とだけ入力してみます。すると……

  1. 「対象プロジェクトを選択してください」 とカード①が表示される
  2. ドロップダウンからプロジェクトを選んで送信すると、裏側でリポジトリとブランチが自動取得され、カード②が表示される
  3. リポジトリ、ブランチ、レビュー範囲(例:ブランチ間差分や特定PR)を選んで 「レビューを開始」 をクリック!

トピックのテスト

レビュー画面

いかがでしょうか! 404 ConnectorRequestFailure エラーは発生しにくくなり、タイポの心配もコピペの手間もない、まるで専用Webアプリのような操作感でAIコードレビューが完遂しました!


まとめ:UIとAIのハイブリッドで「現場で使われるエージェント」へ!

【前編】【後編】の2回にわたり、Microsoft Copilot Studio と Azure DevOps を使った「自社専用AIコードレビューエージェント」の構築方法を解説してきました。

今回の【後編】のポイントをまとめます。

  • アダプティブカードの活用:トピックの質問ノードにアダプティブカードを使うことで、チャット内にリッチな入力フォームを作れる!
  • 動的ドロップダウンでミス撲滅:APIから取得したプロジェクト・リポジトリ・ブランチの一覧からユーザーが選択できるため、タイポや面倒なコピペ作業が一切発生しない!
  • ルールベース×AIのいいとこ取り:「対象の特定とデータ取得」はトピックで確実に制御し、「コードの解析とレビュー指摘」はLLMとナレッジに任せることで、実用性と安定性が劇的に向上!

「AIエージェントを作ってみたけれど、プロンプトの書き方が難しくて現場に定着しない……」というケースはよくありますが、今回のように入り口をUI(アダプティブカード)でガイドしてあげるだけで、チーム全員が迷わず使えるツールに生まれ変わります。

Microsoft 365 Copilot や Copilot Studio のライセンスが社内にある方は、ぜひこの週末に「自社専用のAIシニアエンジニア」を召喚してみてください!


🎉 最後まで読んでいただきありがとうございます!
「前後編を通して参考になった!」「自社でも試してみたい!」と思っていただけた方は、ぜひ :thumbsup:「いいね」 と 「ストック」、そしてフォローをよろしくお願いします!質問や「こんな風にカスタマイズしてみたよ!」といったコメントもお待ちしています!

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?