前編はこちら
💡 この記事のポイント
- 【前編】の課題だった曖昧な指示に起因する 404 エラーを大幅に改善!
- Copilot Studioの「トピック」と「アダプティブカード」を駆使して、チャット内にリッチな選択UI(ドロップダウン)を実装!
- プロジェクト名やブランチ名の手入力・コピペ作業をゼロにし、タイポ知らずの爆速・快適レビュー体験を実現!
はじめに:前編のおさらいと「404エラー」の壁
こんにちは!【前編】の記事では、社内に眠っていた Microsoft Copilot Studio のライセンスと Azure DevOps の標準コネクタを組み合わせて、「自社のコーディング規約(.md)を熟知したAIコードレビューエージェント」 をお手軽に作る方法を解説しました。
プロンプトと標準ツールだけでも驚くほど実用的なレビューをしてくれるようになったのですが、実際に現場で使ってみると 「ある課題」 が浮き彫りになりました。
それは、ユーザーがチャットで 「〇〇についてレビューして」とざっくりした指示を投げたときに発生する 404 HTTP エラー(ConnectorRequestFailure)問題 です。

エージェント側が曖昧なチャットからプロジェクト名やリポジトリ名を推測してAPIの引数にセットしてしまうため、実在しない名前を指定してしまいエラーで止まってしまうのです。かといって、毎回人間が一字一句正確なプロジェクト名やリポジトリ名、長いブランチ名を調べてコピペするのは本末転倒ですよね。
「チャットの中で、プロジェクトやブランチをプルダウンからポチポチ選ぶだけでレビューできたら最高なのに……!」
そこで本記事(後編)では、Copilot Studioの 「トピック」 と 「アダプティブカード(Adaptive Cards)」 を駆使して、Azure DevOpsからリアルタイムに一覧を取得し、 UI上で確実にレビュー対象を指定できる超快適なエージェントへの進化方法 を徹底解説します!
解決のアプローチ:「トピック」×「アダプティブカード」で選択UIを作る!
今回の改善のキモは、AIの自由な推測(オーケストレーション)にすべてを任せるのではなく、「対象を特定する部分」をCopilot Studioの「トピック(会話フロー)」と「アダプティブカード」でカチッと制御してあげることです。
具体的には、以下のような流れを作ります。
- ユーザーがレビューを依頼するとトピックが起動
- エージェントが裏側で 「プロジェクト一覧」 をAPI取得し、ドロップダウンUIで提示
- ユーザーがプロジェクトを選ぶと、それに紐づく 「リポジトリ一覧」「ブランチ一覧」 をAPI取得
- 2つ目のカードUIで、リポジトリ・ブランチ・レビュー範囲(特定PR / ブランチ差分 / 特定ファイル / フォルダ全体) を選択・入力
- 確定した正確な情報をもとにソースコードを取得し、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選択式のトピックを組み込んでいきます。
- トピックの新規作成
-
トピックのロジック設定 👈 アダプティブカードと分岐処理のキモ!
- Step 2-a: Azure DevOpsプロジェクト一覧の取得と選択UI
- Step 2-b: リポジトリ&ブランチ一覧の取得と詳細条件の入力UI
- Step 2-c: レビュー範囲に応じたデータ取得とAIレビューへの引き継ぎ
- トピックのテストと動作確認
それでは、さっそく作っていきましょう!
1. トピックを作成
まずは、Copilot Studioの上部メニューから**「トピック」**タブを開きます。
「+ トピックを追加」から新しいカスタムトピックを作成し、ユーザーが「レビューして」「コードを見て」などと話しかけた際にこのトピックが起動するようにトリガーを設定します。
2. トピックのロジック設定
ここが後編のメインパートです!フローチャートに沿って、順番にノードを組み立てていきます。
Step 2-a. Azure DevOpsプロジェクト一覧から選択させる(カード①)
まずは、ユーザーに対象のプロジェクトを選んでもらうための処理を作ります。
1. メッセージノードの追加
- 「レビュー条件を入力してください」と案内を表示します
2. ツール(コネクタ)でプロジェクト一覧を取得
3. アダプティブカードでドロップダウンUIを表示(カード①)
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/commitsAPI は、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. トピックのテスト
すべてのロジックが組めたら、画面右側のテストチャットで実際に動かしてみましょう!
チャット欄に「レビューして」とだけ入力してみます。すると……
- 「対象プロジェクトを選択してください」 とカード①が表示される
- ドロップダウンからプロジェクトを選んで送信すると、裏側でリポジトリとブランチが自動取得され、カード②が表示される
- リポジトリ、ブランチ、レビュー範囲(例:ブランチ間差分や特定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シニアエンジニア」を召喚してみてください!
🎉 最後まで読んでいただきありがとうございます!
「前後編を通して参考になった!」「自社でも試してみたい!」と思っていただけた方は、ぜひ
「いいね」 と 「ストック」、そしてフォローをよろしくお願いします!質問や「こんな風にカスタマイズしてみたよ!」といったコメントもお待ちしています!














