diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml
index ad17593..7e897ca 100644
--- a/.github/workflows/pr-review.yml
+++ b/.github/workflows/pr-review.yml
@@ -38,9 +38,10 @@ jobs:
--owner "${{ github.event.pull_request.head.repo.owner.login || github.repository_owner }}" \
--repo "${{ github.event.pull_request.head.repo.name || github.event.repository.name }}" \
--pr "${{ github.event.pull_request.number }}" \
+ --language ja \
--post-comment
env:
- AISettings__Endpoint: ${{ vars.AI_ENDPOINT || 'https://api.openai.com/v1' }}
- AISettings__ApiKey: ${{ secrets.AI_API_KEY }}
- AISettings__ModelId: ${{ vars.AI_MODEL_ID || 'gpt-4o-mini' }}
+ AISettings__Endpoint: ${{ vars.AISETTINGS__ENDPOINT }}
+ AISettings__ApiKey: ${{ secrets.AISETTINGS__APIKEY }}
+ AISettings__ModelId: ${{ vars.AISETTINGS__MODELID || 'gpt-4o-mini' }}
PRSettings__GitHubToken: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pragent-native.yml b/.github/workflows/pragent-native.yml
index 3ca5c9f..f840c2d 100644
--- a/.github/workflows/pragent-native.yml
+++ b/.github/workflows/pragent-native.yml
@@ -1,25 +1,9 @@
# PRAgent - Native .NET CLI Version
-# PR作成/更新時に自動実行、または他のリポジトリから参照して使用
+# 他のリポジトリから参照して使用
name: PRAgent (Native)
on:
- pull_request:
- types: [opened, synchronize]
- workflow_dispatch:
- inputs:
- pr_number:
- description: 'PR number'
- required: true
- type: number
- command:
- description: 'Command (review/summary/approve)'
- required: true
- type: choice
- options:
- - review
- - summary
- - approve
workflow_call:
inputs:
pr_number:
diff --git a/PRAgent.Tests/CommentCommandTests.cs b/PRAgent.Tests/CommentCommandTests.cs
new file mode 100644
index 0000000..6ef5bd1
--- /dev/null
+++ b/PRAgent.Tests/CommentCommandTests.cs
@@ -0,0 +1,320 @@
+using PRAgent.Models;
+using Xunit;
+
+namespace PRAgent.Tests;
+
+public class CommentCommandTests
+{
+ [Fact]
+ public void Parse_CommentCommandWithSingleComment_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@150", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(150, options.Comments[0].LineNumber);
+ Assert.Equal("src/index.cs", options.Comments[0].FilePath);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.Null(options.Comments[0].SuggestionText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithMultipleComments_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@100", "Comment1", "@200", "Comment2" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Equal(2, options.Comments.Count);
+ Assert.Equal(100, options.Comments[0].LineNumber);
+ Assert.Equal("Comment1", options.Comments[0].CommentText);
+ Assert.Equal(200, options.Comments[1].LineNumber);
+ Assert.Equal("Comment2", options.Comments[1].CommentText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithSuggestion_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@200", "Test comment", "--suggestion", "Suggested code" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(200, options.Comments[0].LineNumber);
+ Assert.Equal("src/index.cs", options.Comments[0].FilePath);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.Equal("Suggested code", options.Comments[0].SuggestionText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithShortOptions_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "-o", "testowner", "-r", "testrepo", "-p", "123", "@150", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(150, options.Comments[0].LineNumber);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithoutOwner_ReturnsError()
+ {
+ // Arrange
+ string[] args = { "comment", "--repo", "testrepo", "--pr", "123", "@150", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+ var isValid = options.IsValid(out var errors);
+
+ // Assert
+ Assert.False(isValid);
+ Assert.Contains("--owner is required", errors);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithoutRepo_ReturnsError()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--pr", "123", "@150", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+ var isValid = options.IsValid(out var errors);
+
+ // Assert
+ Assert.False(isValid);
+ Assert.Contains("--repo is required", errors);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithoutPrNumber_ReturnsError()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "@150", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+ var isValid = options.IsValid(out var errors);
+
+ // Assert
+ Assert.False(isValid);
+ Assert.Contains("--pr is required and must be a positive number", errors);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithoutComments_ReturnsError()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+ var isValid = options.IsValid(out var errors);
+
+ // Assert
+ Assert.False(isValid);
+ // コメントが指定されていない場合はエラー
+ Assert.Contains("No valid comments specified", errors);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithInvalidLineNumber_IsIgnored()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@-1", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ // 無効な行番号のコメントはパース段階で除外される
+ Assert.Empty(options.Comments);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithEmptyCommentText_IsIgnored()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@150", "" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ // 空のコメントはパース段階で除外される
+ Assert.Empty(options.Comments);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithWhitespaceOnlyCommentText_IsIgnored()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@150", " \t\n " };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ // 空白のみのコメントはパース段階で除外される
+ Assert.Empty(options.Comments);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithEmptySuggestion_IgnoresSuggestion()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@200", "Test comment", "--suggestion", "" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Single(options.Comments);
+ Assert.Equal(200, options.Comments[0].LineNumber);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.Null(options.Comments[0].SuggestionText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithWhitespaceOnlySuggestion_IgnoresSuggestion()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@200", "Test comment", "--suggestion", " \t\n " };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Single(options.Comments);
+ Assert.Equal(200, options.Comments[0].LineNumber);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.Null(options.Comments[0].SuggestionText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithFilePath_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "src/file.cs@123", "Test comment" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(123, options.Comments[0].LineNumber);
+ Assert.Equal("src/file.cs", options.Comments[0].FilePath);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.Null(options.Comments[0].SuggestionText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithApprove_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@150", "Test comment", "--approve" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(150, options.Comments[0].LineNumber);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.True(options.Approve);
+ Assert.Null(options.Comments[0].SuggestionText);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithApproveAndSuggestion_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@200", "Test comment", "--suggestion", "Fixed code", "--approve" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(200, options.Comments[0].LineNumber);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.Equal("Fixed code", options.Comments[0].SuggestionText);
+ Assert.True(options.Approve);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithMultipleCommentsAndApprove_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "--owner", "testowner", "--repo", "testrepo", "--pr", "123", "@100", "Comment1", "@200", "Comment2", "--approve" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Equal(2, options.Comments.Count);
+ Assert.Equal(100, options.Comments[0].LineNumber);
+ Assert.Equal("Comment1", options.Comments[0].CommentText);
+ Assert.Equal(200, options.Comments[1].LineNumber);
+ Assert.Equal("Comment2", options.Comments[1].CommentText);
+ Assert.True(options.Approve);
+ }
+
+ [Fact]
+ public void Parse_CommentCommandWithShortApprove_ReturnsCorrectOptions()
+ {
+ // Arrange
+ string[] args = { "comment", "-o", "testowner", "-r", "testrepo", "-p", "123", "@150", "Test comment", "--approve" };
+
+ // Act
+ var options = CommentCommandOptions.Parse(args);
+
+ // Assert
+ Assert.Equal("testowner", options.Owner);
+ Assert.Equal("testrepo", options.Repo);
+ Assert.Equal(123, options.PrNumber);
+ Assert.Single(options.Comments);
+ Assert.Equal(150, options.Comments[0].LineNumber);
+ Assert.Equal("Test comment", options.Comments[0].CommentText);
+ Assert.True(options.Approve);
+ }
+}
\ No newline at end of file
diff --git a/PRAgent.Tests/DiffPositionCalculatorTests.cs b/PRAgent.Tests/DiffPositionCalculatorTests.cs
new file mode 100644
index 0000000..3445cf2
--- /dev/null
+++ b/PRAgent.Tests/DiffPositionCalculatorTests.cs
@@ -0,0 +1,225 @@
+using Xunit;
+
+namespace PRAgent.Tests;
+
+///
+/// DiffPositionCalculatorのテスト
+///
+public class DiffPositionCalculatorTests
+{
+ ///
+ /// テスト用のdiff計算メソッド(GitHubServiceと同じロジック)
+ ///
+ private static int? CalculateDiffPosition(string? patch, int lineNumber)
+ {
+ if (string.IsNullOrEmpty(patch))
+ return null;
+
+ var lines = patch.Split('\n');
+ int position = 0;
+ int currentNewLine = 0;
+
+ foreach (var line in lines)
+ {
+ position++;
+
+ // Hunk headerを解析
+ var hunkMatch = System.Text.RegularExpressions.Regex.Match(line, @"^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@");
+ if (hunkMatch.Success)
+ {
+ // 開始行番号の1つ前に設定(次の行でインクリメントして正しい行番号になるように)
+ currentNewLine = int.Parse(hunkMatch.Groups[1].Value) - 1;
+ continue;
+ }
+
+ // 行のタイプを判定
+ if (line.StartsWith("+"))
+ {
+ currentNewLine++;
+ if (currentNewLine == lineNumber)
+ {
+ return position;
+ }
+ }
+ else if (line.StartsWith("-"))
+ {
+ // 削除行はスキップ
+ }
+ else if (line.StartsWith(" ") || line == "")
+ {
+ currentNewLine++;
+ if (currentNewLine == lineNumber)
+ {
+ return position;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_SimpleAddition_ReturnsCorrectPosition()
+ {
+ // Arrange
+ // diffは以下のようになる(行番号はdiff内のposition):
+ // 1: @@ -1,3 +1,4 @@ <- hunk header (position=1)
+ // 2: line1 <- position=2, ファイル内1行目
+ // 3: line2 <- position=3, ファイル内2行目
+ // 4: +newLine <- position=4, ファイル内3行目(追加)
+ // 5: line3 <- position=5, ファイル内4行目
+ var patch = "@@ -1,3 +1,4 @@\n line1\n line2\n+newLine\n line3";
+
+ // Act & Assert
+ Assert.Equal(2, CalculateDiffPosition(patch, 1)); // line1
+ Assert.Equal(3, CalculateDiffPosition(patch, 2)); // line2
+ Assert.Equal(4, CalculateDiffPosition(patch, 3)); // newLine (added)
+ Assert.Equal(5, CalculateDiffPosition(patch, 4)); // line3
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_AddedLine_ReturnsCorrectPosition()
+ {
+ // Arrange
+ var patch = "@@ -1,3 +1,4 @@\n line1\n line2\n+newLine\n line3";
+
+ // Act - newLineはファイル内の3行目(追加された行)
+ var result = CalculateDiffPosition(patch, 3);
+
+ // Assert - position 4は "+newLine" の行
+ Assert.Equal(4, result);
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_AfterAddedLine_ReturnsCorrectPosition()
+ {
+ // Arrange
+ var patch = "@@ -1,3 +1,4 @@\n line1\n line2\n+newLine\n line3";
+
+ // Act - line3は追加後のファイル内の4行目
+ var result = CalculateDiffPosition(patch, 4);
+
+ // Assert - position 5は " line3" の行
+ // diff: 1=hunk, 2=line1, 3=line2, 4=+newLine, 5=line3
+ Assert.Equal(5, result);
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_WithDeletion_SkipsDeletedLine()
+ {
+ // Arrange
+ // diff:
+ // 1: @@ -1,4 +1,3 @@
+ // 2: line1 <- ファイル内1行目
+ // 3: -oldLine2 <- 削除行(ファイル内行番号は進まない)
+ // 4: line3 <- ファイル内2行目
+ // 5: line4 <- ファイル内3行目
+ var patch = "@@ -1,4 +1,3 @@\n line1\n-oldLine2\n line3\n line4";
+
+ // Act & Assert
+ Assert.Equal(2, CalculateDiffPosition(patch, 1)); // line1
+ Assert.Equal(4, CalculateDiffPosition(patch, 2)); // line3 (削除行の後)
+ Assert.Equal(5, CalculateDiffPosition(patch, 3)); // line4
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_LineNotFound_ReturnsNull()
+ {
+ // Arrange
+ var patch = "@@ -1,2 +1,2 @@\n line1\n line2";
+
+ // Act - 行番号100は存在しない
+ var result = CalculateDiffPosition(patch, 100);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_EmptyPatch_ReturnsNull()
+ {
+ // Arrange
+ var patch = "";
+
+ // Act
+ var result = CalculateDiffPosition(patch, 1);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_NullPatch_ReturnsNull()
+ {
+ // Arrange
+ string? patch = null;
+
+ // Act
+ var result = CalculateDiffPosition(patch, 1);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_MultipleHunks_ReturnsCorrectPosition()
+ {
+ // Arrange - 複数のhunkがあるケース
+ // 1: @@ -1,3 +1,4 @@ <- hunk 1 header
+ // 2: line1
+ // 3: line2
+ // 4: +newLine
+ // 5: line3
+ // 6: @@ -10,3 +11,4 @@ <- hunk 2 header (ファイル内11行目から開始)
+ // 7: line10 <- ファイル内11行目
+ // 8: line11 <- ファイル内12行目
+ // 9: +anotherNewLine <- ファイル内13行目
+ // 10: line12 <- ファイル内14行目
+ var patch = "@@ -1,3 +1,4 @@\n line1\n line2\n+newLine\n line3\n@@ -10,3 +11,4 @@\n line10\n line11\n+anotherNewLine\n line12";
+
+ // Act & Assert
+ Assert.Equal(9, CalculateDiffPosition(patch, 13)); // anotherNewLine
+ Assert.Equal(10, CalculateDiffPosition(patch, 14)); // line12
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_HunkStartLine_ReturnsCorrectPosition()
+ {
+ // Arrange - hunkが+15から始まる場合
+ // ファイル内の15行目からdiffが始まる
+ var patch = "@@ -10,3 +15,4 @@\n line15\n line16\n+newLine\n line17";
+
+ // Act & Assert
+ Assert.Equal(2, CalculateDiffPosition(patch, 15)); // line15
+ Assert.Equal(3, CalculateDiffPosition(patch, 16)); // line16
+ Assert.Equal(4, CalculateDiffPosition(patch, 17)); // newLine
+ Assert.Equal(5, CalculateDiffPosition(patch, 18)); // line17
+ }
+
+ [Fact]
+ public void CalculateDiffPosition_RealWorldExample_WorksCorrectly()
+ {
+ // Arrange - 実際のGitHubのdiff例
+ // hunkは+15から始まるので、ファイル内の15行目がdiffの最初の行
+ var patch = "@@ -15,8 +15,10 @@ public class Example\n public void Method1()\n {\n var x = 1;\n+ var y = 2;\n+ var z = 3;\n Console.WriteLine(x);\n- Console.WriteLine(\"old\");\n+ Console.WriteLine(\"new\");\n }\n}";
+
+ // diff position:
+ // 1: @@ -15,8 +15,10 @@ public class Example
+ // 2: public void Method1() <- ファイル内15行目
+ // 3: { <- ファイル内16行目
+ // 4: var x = 1; <- ファイル内17行目
+ // 5: + var y = 2; <- ファイル内18行目(追加)
+ // 6: + var z = 3; <- ファイル内19行目(追加)
+ // 7: Console.WriteLine(x); <- ファイル内20行目
+ // 8: - Console.WriteLine("old"); <- 削除行
+ // 9: + Console.WriteLine("new"); <- ファイル内21行目(追加)
+ // 10: }
+ // 11: }
+
+ // Act & Assert
+ Assert.Equal(5, CalculateDiffPosition(patch, 18)); // var y = 2
+ Assert.Equal(6, CalculateDiffPosition(patch, 19)); // var z = 3
+ Assert.Equal(7, CalculateDiffPosition(patch, 20)); // Console.WriteLine(x)
+ Assert.Equal(9, CalculateDiffPosition(patch, 21)); // Console.WriteLine("new")
+ }
+}
diff --git a/PRAgent.Tests/DraftPullRequestReviewCommentTests.cs b/PRAgent.Tests/DraftPullRequestReviewCommentTests.cs
new file mode 100644
index 0000000..6de9d34
--- /dev/null
+++ b/PRAgent.Tests/DraftPullRequestReviewCommentTests.cs
@@ -0,0 +1,112 @@
+using Octokit;
+using Xunit;
+
+namespace PRAgent.Tests;
+
+///
+/// DraftPullRequestReviewCommentの引数順序が正しいことを確認するテスト
+///
+public class DraftPullRequestReviewCommentTests
+{
+ [Fact]
+ public void Constructor_ParameterOrder_ShouldBeBodyPathPosition()
+ {
+ // Arrange
+ var body = "This is a test comment";
+ var path = "src/test/File.cs";
+ var position = 42;
+
+ // Act
+ var comment = new DraftPullRequestReviewComment(body, path, position);
+
+ // Assert - Octokitの仕様通り (body, path, position) の順序であることを確認
+ Assert.Equal(body, comment.Body);
+ Assert.Equal(path, comment.Path);
+ Assert.Equal(position, comment.Position);
+ }
+
+ [Fact]
+ public void Constructor_WithJapaneseBody_ShouldWork()
+ {
+ // Arrange
+ var body = "これはテストコメントです。修正が必要です。";
+ var path = "src/test/File.cs";
+ var position = 10;
+
+ // Act
+ var comment = new DraftPullRequestReviewComment(body, path, position);
+
+ // Assert
+ Assert.Equal(body, comment.Body);
+ Assert.Equal(path, comment.Path);
+ Assert.Equal(position, comment.Position);
+ }
+
+ [Fact]
+ public void Constructor_WithSuggestionInBody_ShouldWork()
+ {
+ // Arrange
+ var body = "Please fix this\n```suggestion\nvar fixed = true;\n```";
+ var path = "src/test/File.cs";
+ var position = 15;
+
+ // Act
+ var comment = new DraftPullRequestReviewComment(body, path, position);
+
+ // Assert
+ Assert.Equal(body, comment.Body);
+ Assert.Contains("```suggestion", comment.Body);
+ }
+
+ [Fact]
+ public void Constructor_WithNullBody_ShouldThrow()
+ {
+ // Arrange
+ string? body = null;
+ var path = "src/test/File.cs";
+ var position = 1;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ new DraftPullRequestReviewComment(body!, path, position));
+ }
+
+ [Fact]
+ public void Constructor_WithNullPath_ShouldThrow()
+ {
+ // Arrange
+ var body = "Test comment";
+ string? path = null;
+ var position = 1;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ new DraftPullRequestReviewComment(body, path!, position));
+ }
+
+ [Fact]
+ public void Constructor_WithEmptyBody_ShouldThrow()
+ {
+ // Arrange
+ var body = "";
+ var path = "src/test/File.cs";
+ var position = 1;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ new DraftPullRequestReviewComment(body, path, position));
+ }
+
+ [Fact]
+ public void Constructor_WithEmptyPath_ShouldThrow()
+ {
+ // Arrange
+ var body = "Test comment";
+ var path = "";
+ var position = 1;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ new DraftPullRequestReviewComment(body, path, position));
+ }
+}
diff --git a/PRAgent.Tests/GitHubServiceLineCommentTests.cs b/PRAgent.Tests/GitHubServiceLineCommentTests.cs
new file mode 100644
index 0000000..a248f85
--- /dev/null
+++ b/PRAgent.Tests/GitHubServiceLineCommentTests.cs
@@ -0,0 +1,145 @@
+using Moq;
+using Octokit;
+using PRAgent.Services;
+using Xunit;
+
+namespace PRAgent.Tests;
+
+///
+/// GitHubServiceの行コメント機能のテスト
+///
+public class GitHubServiceLineCommentTests
+{
+ ///
+ /// DraftPullRequestReviewCommentのリストが正しく作成されることを確認するテスト
+ ///
+ [Fact]
+ public void DraftPullRequestReviewComment_CreatesCorrectComment_ForSingleLine()
+ {
+ // Arrange
+ var body = "This needs to be fixed";
+ var path = "src/Services/GitHubService.cs";
+ var position = 42;
+
+ // Act
+ var comment = new DraftPullRequestReviewComment(body, path, position);
+
+ // Assert
+ Assert.Equal(body, comment.Body);
+ Assert.Equal(path, comment.Path);
+ Assert.Equal(position, comment.Position);
+ }
+
+ ///
+ /// 複数行コメントのデータ構造が正しく作成されることを確認するテスト
+ ///
+ [Fact]
+ public void MultipleLineComments_CreatesCorrectDataStructure()
+ {
+ // Arrange & Act
+ var comments = new List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)>
+ {
+ ("src/File1.cs", 10, null, null, "Fix this issue", null),
+ ("src/File2.cs", 20, null, null, "Another issue", "var x = 1;"),
+ ("src/File3.cs", null, 30, 35, "Multi-line issue", null)
+ };
+
+ // Assert
+ Assert.Equal(3, comments.Count);
+ Assert.Equal("src/File1.cs", comments[0].FilePath);
+ Assert.Equal(10, comments[0].LineNumber);
+ Assert.Equal("Fix this issue", comments[0].Comment);
+
+ Assert.Equal("src/File2.cs", comments[1].FilePath);
+ Assert.Equal(20, comments[1].LineNumber);
+ Assert.Equal("var x = 1;", comments[1].Suggestion);
+
+ Assert.Equal("src/File3.cs", comments[2].FilePath);
+ Assert.Null(comments[2].LineNumber);
+ Assert.Equal(30, comments[2].StartLine);
+ Assert.Equal(35, comments[2].EndLine);
+ }
+
+ ///
+ /// DraftPullRequestReviewCommentのリストが正しく作成されることを確認するテスト
+ ///
+ [Fact]
+ public void DraftPullRequestReviewComment_CreatesCorrectList()
+ {
+ // Arrange
+ var commentsData = new List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)>
+ {
+ ("src/File1.cs", 10, null, null, "Issue 1", null),
+ ("src/File2.cs", 20, null, null, "Issue 2", "var x = 1;")
+ };
+
+ // Act
+ var draftComments = commentsData.Select(c =>
+ {
+ var commentBody = c.Suggestion != null ? $"{c.Comment}\n```suggestion\n{c.Suggestion}\n```" : c.Comment;
+
+ if (c.LineNumber.HasValue)
+ {
+ return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.LineNumber.Value);
+ }
+ else if (c.StartLine.HasValue)
+ {
+ return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.StartLine.Value);
+ }
+ else
+ {
+ throw new ArgumentException($"Comment must have either LineNumber or StartLine: {c.FilePath}");
+ }
+ }).ToList();
+
+ // Assert
+ Assert.Equal(2, draftComments.Count);
+
+ Assert.Equal("Issue 1", draftComments[0].Body);
+ Assert.Equal("src/File1.cs", draftComments[0].Path);
+ Assert.Equal(10, draftComments[0].Position);
+
+ Assert.Contains("Issue 2", draftComments[1].Body);
+ Assert.Contains("```suggestion", draftComments[1].Body);
+ Assert.Contains("var x = 1;", draftComments[1].Body);
+ Assert.Equal("src/File2.cs", draftComments[1].Path);
+ Assert.Equal(20, draftComments[1].Position);
+ }
+
+ ///
+ /// サジェスチョン付きコメントのフォーマットが正しいことを確認するテスト
+ ///
+ [Fact]
+ public void CommentWithSuggestion_FormatsCorrectly()
+ {
+ // Arrange
+ var comment = "Please use var instead";
+ var suggestion = "var number = 42;";
+
+ // Act
+ var commentBody = $"{comment}\n```suggestion\n{suggestion}\n```";
+
+ // Assert
+ Assert.Equal("Please use var instead\n```suggestion\nvar number = 42;\n```", commentBody);
+ }
+
+ ///
+ /// 日本語コメントでも正しく動作することを確認するテスト
+ ///
+ [Fact]
+ public void CommentWithJapanese_WorksCorrectly()
+ {
+ // Arrange
+ var body = "この変数名は分かりにくいです。より説明的な名前に変更することを検討してください。";
+ var path = "src/サービス/メイン.cs";
+ var position = 100;
+
+ // Act
+ var comment = new DraftPullRequestReviewComment(body, path, position);
+
+ // Assert
+ Assert.Equal(body, comment.Body);
+ Assert.Equal(path, comment.Path);
+ Assert.Equal(position, comment.Position);
+ }
+}
diff --git a/PRAgent.Tests/YamlConfigurationProviderTests.cs b/PRAgent.Tests/YamlConfigurationProviderTests.cs
deleted file mode 100644
index 39affab..0000000
--- a/PRAgent.Tests/YamlConfigurationProviderTests.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using PRAgent.Configuration;
-
-namespace PRAgent.Tests;
-
-public class YamlConfigurationProviderTests
-{
- [Fact]
- public void Deserialize_ValidYaml_ReturnsConfig()
- {
- // Arrange
- var yaml = """
- pragent:
- enabled: true
- system_prompt: "Test prompt"
- review:
- enabled: true
- auto_post: false
- summary:
- enabled: true
- post_as_comment: true
- approve:
- enabled: true
- auto_approve_threshold: "minor"
- require_review_first: true
- ignore_paths:
- - "*.min.js"
- - "dist/**"
- """;
-
- // Act
- var result = YamlConfigurationProvider.Deserialize(yaml);
-
- // Assert
- Assert.NotNull(result);
- Assert.NotNull(result.PRAgent);
- Assert.True(result.PRAgent.Enabled);
- Assert.Equal("Test prompt", result.PRAgent.SystemPrompt);
- Assert.True(result.PRAgent.Review?.Enabled);
- Assert.False(result.PRAgent.Review?.AutoPost);
- Assert.True(result.PRAgent.Summary?.Enabled);
- Assert.True(result.PRAgent.Summary?.PostAsComment);
- Assert.True(result.PRAgent.Approve?.Enabled);
- Assert.Equal("minor", result.PRAgent.Approve?.AutoApproveThreshold);
- Assert.True(result.PRAgent.Approve?.RequireReviewFirst);
- Assert.NotNull(result.PRAgent.IgnorePaths);
- Assert.Equal(2, result.PRAgent.IgnorePaths?.Count);
- }
-
- [Fact]
- public void Deserialize_EmptyYaml_ReturnsNull()
- {
- // Arrange
- var yaml = "";
-
- // Act
- var result = YamlConfigurationProvider.Deserialize(yaml);
-
- // Assert
- Assert.Null(result);
- }
-
- [Fact]
- public void Deserialize_NullYaml_ReturnsNull()
- {
- // Act
- var result = YamlConfigurationProvider.Deserialize(null!);
-
- // Assert
- Assert.Null(result);
- }
-
- [Fact]
- public void IsValidYaml_ValidYaml_ReturnsTrue()
- {
- // Arrange
- var yaml = """
- pragent:
- enabled: true
- """;
-
- // Act
- var result = YamlConfigurationProvider.IsValidYaml(yaml);
-
- // Assert
- Assert.True(result);
- }
-
- [Fact]
- public void IsValidYaml_InvalidYaml_ReturnsFalse()
- {
- // Arrange
- var yaml = """
- pragent:
- enabled: [unclosed bracket
- """;
-
- // Act
- var result = YamlConfigurationProvider.IsValidYaml(yaml);
-
- // Assert
- Assert.False(result);
- }
-
- [Fact]
- public void IsValidYaml_EmptyString_ReturnsFalse()
- {
- // Act
- var result = YamlConfigurationProvider.IsValidYaml("");
-
- // Assert
- Assert.False(result);
- }
-
- [Fact]
- public void Deserialize_MinimalYaml_ReturnsConfigWithDefaults()
- {
- // Arrange
- var yaml = """
- pragent:
- enabled: true
- """;
-
- // Act
- var result = YamlConfigurationProvider.Deserialize(yaml);
-
- // Assert
- Assert.NotNull(result);
- Assert.NotNull(result.PRAgent);
- Assert.True(result.PRAgent.Enabled);
- Assert.Null(result.PRAgent.SystemPrompt);
- Assert.Null(result.PRAgent.Review);
- Assert.Null(result.PRAgent.Summary);
- Assert.Null(result.PRAgent.Approve);
- Assert.Null(result.PRAgent.IgnorePaths);
- }
-}
diff --git a/PRAgent.Tests/packages.lock.json b/PRAgent.Tests/packages.lock.json
index 827b938..10ad3ab 100644
--- a/PRAgent.Tests/packages.lock.json
+++ b/PRAgent.Tests/packages.lock.json
@@ -63,6 +63,16 @@
"System.Memory.Data": "8.0.1"
}
},
+ "Azure.Identity": {
+ "type": "Transitive",
+ "resolved": "1.17.0",
+ "contentHash": "QZiMa1O5lTniWTSDPyPVdBKOS0/M5DWXTfQ2xLOot96yp8Q5A69iScGtzCIxfbg/4bmp/TynibZ2VK1v3qsSNA==",
+ "dependencies": {
+ "Azure.Core": "1.49.0",
+ "Microsoft.Identity.Client": "4.76.0",
+ "Microsoft.Identity.Client.Extensions.Msal": "4.76.0"
+ }
+ },
"Castle.Core": {
"type": "Transitive",
"resolved": "5.1.1",
@@ -71,6 +81,16 @@
"System.Diagnostics.EventLog": "6.0.0"
}
},
+ "Microsoft.Agents.AI.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.0.0-preview.251009.1",
+ "contentHash": "8Bfppenx9ETyhDsJ30Hixy/zf/t2t/vt03+jl7uLd5XgadZo+xQcEYa16ANHUAcJzBH7ou85sSRO8MohNiFgiw==",
+ "dependencies": {
+ "Microsoft.Extensions.AI.Abstractions": "9.9.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.9",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.9"
+ }
+ },
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -408,6 +428,28 @@
"Microsoft.Extensions.AI.Abstractions": "9.5.0"
}
},
+ "Microsoft.Identity.Client": {
+ "type": "Transitive",
+ "resolved": "4.76.0",
+ "contentHash": "j2FtmljuCveDJ7umBVYm6Bx3iVGA71U07Dc7byGr2Hrj7XlByZSknruCBUeYN3V75nn1VEhXegxE0MerxvxrXQ==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Abstractions": "6.35.0"
+ }
+ },
+ "Microsoft.Identity.Client.Extensions.Msal": {
+ "type": "Transitive",
+ "resolved": "4.76.0",
+ "contentHash": "D0n3yMTa3gnDh1usrJmyxGWKYeqbQNCWLgQ0Tswf7S6nk9YobiCOX+M2V8EX5SPqkZxpwkTT6odAhaafu3TIeA==",
+ "dependencies": {
+ "Microsoft.Identity.Client": "4.76.0",
+ "System.Security.Cryptography.ProtectedData": "4.5.0"
+ }
+ },
+ "Microsoft.IdentityModel.Abstractions": {
+ "type": "Transitive",
+ "resolved": "6.35.0",
+ "contentHash": "xuR8E4Rd96M41CnUSCiOJ2DBh+z+zQSmyrYHdYhD6K4fXBcQGVnRCFQ0efROUYpP+p0zC1BLKr0JRpVuujTZSg=="
+ },
"Microsoft.SemanticKernel": {
"type": "Transitive",
"resolved": "1.68.0",
@@ -427,6 +469,36 @@
"Microsoft.Extensions.VectorData.Abstractions": "9.7.0"
}
},
+ "Microsoft.SemanticKernel.Agents.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.68.0",
+ "contentHash": "hkKxc0Fuy4+ax3HjDyGtzm0EESEX0Qq/Q2vEYjCe2x9KEsiAAU+droPeqrWoy48+u0o01oGkA0IgA07u9Ta/jw==",
+ "dependencies": {
+ "Microsoft.Agents.AI.Abstractions": "1.0.0-preview.251009.1",
+ "Microsoft.SemanticKernel.Abstractions": "1.68.0"
+ }
+ },
+ "Microsoft.SemanticKernel.Agents.Core": {
+ "type": "Transitive",
+ "resolved": "1.68.0",
+ "contentHash": "b+3d4cHXLKSg73BZLlqGUf0jb254fVdYCH5gQiUYHNLEoGNRk5JkUt6gr0p0Uomp+ooBz7tFfhWYEpBsTni2Bw==",
+ "dependencies": {
+ "Microsoft.SemanticKernel.Agents.Abstractions": "1.68.0",
+ "Microsoft.SemanticKernel.Core": "1.68.0"
+ }
+ },
+ "Microsoft.SemanticKernel.Agents.OpenAI": {
+ "type": "Transitive",
+ "resolved": "1.68.0-preview",
+ "contentHash": "I5GQkj9BdzI5e1y1iWl6BPkZCbA5IqC+07CraTa/Y+/EGDI88RO4UEoClmyq7bGv/hF+9eFD5pasWmhWnWGf1Q==",
+ "dependencies": {
+ "Azure.AI.OpenAI": "2.7.0-beta.2",
+ "Azure.Identity": "1.17.0",
+ "Microsoft.SemanticKernel.Agents.Abstractions": "1.68.0",
+ "Microsoft.SemanticKernel.Agents.Core": "1.68.0",
+ "Microsoft.SemanticKernel.Connectors.OpenAI": "1.68.0"
+ }
+ },
"Microsoft.SemanticKernel.Connectors.AzureOpenAI": {
"type": "Transitive",
"resolved": "1.68.0",
@@ -560,6 +632,11 @@
"resolved": "10.0.0",
"contentHash": "PxZKFx8ACAt7gRnTSDgtZFA06cimls5XL1Olv63bWF8qKL5+EQdqq3K02etW4LNut4xDoREPe9z7QYATLMkBPA=="
},
+ "System.Security.Cryptography.ProtectedData": {
+ "type": "Transitive",
+ "resolved": "4.5.0",
+ "contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q=="
+ },
"xunit.abstractions": {
"type": "Transitive",
"resolved": "2.0.3",
@@ -615,6 +692,8 @@
"Microsoft.Extensions.Hosting": "[10.0.0, )",
"Microsoft.Extensions.Logging": "[10.0.0, )",
"Microsoft.SemanticKernel": "[1.68.0, )",
+ "Microsoft.SemanticKernel.Agents.Core": "[1.68.0, )",
+ "Microsoft.SemanticKernel.Agents.OpenAI": "[1.68.0-preview, )",
"Microsoft.SemanticKernel.Connectors.OpenAI": "[1.68.0, )",
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "[1.23.0, )",
"Octokit": "[14.0.0, )",
diff --git a/PRAgent/Agents/AgentDefinition.cs b/PRAgent/Agents/AgentDefinition.cs
index 4ab7b24..72a2b3a 100644
--- a/PRAgent/Agents/AgentDefinition.cs
+++ b/PRAgent/Agents/AgentDefinition.cs
@@ -181,8 +181,13 @@ You are an expert code reviewer with deep knowledge of software engineering best
- Code organization and readability
- Adherence to best practices and design patterns
- Test coverage and quality
+
+ After completing the review, you can:
+ - Approve the PR if no major issues are found
+ - Request changes if there are issues that need to be addressed
+ - Add line comments for specific issues
""",
- description: "Reviews pull requests for code quality, security, and best practices"
+ description: "Reviews pull requests for code quality, security, and best practices. Can approve or request changes."
);
public static AgentDefinition DetailedCommentAgent => new(
@@ -213,44 +218,4 @@ 4. Detailed comment body with suggestions
""",
description: "Creates structured review comments for detailed line-by-line review"
);
-
- public static AgentDefinition ApprovalAgent => new(
- name: "ApprovalAgent",
- role: "Approval Authority",
- systemPrompt: """
- You are a senior technical lead responsible for making approval decisions on pull requests.
-
- Your role is to:
- 1. Analyze code review results
- 2. Evaluate findings against approval thresholds
- 3. Make conservative, risk-aware approval decisions
- 4. Provide clear reasoning for your decisions
-
- Approval thresholds:
- - critical: PR must have NO critical issues
- - major: PR must have NO major or critical issues
- - minor: PR must have NO minor, major, or critical issues
- - none: Always approve
-
- When in doubt, err on the side of caution and recommend rejection or additional review.
- """,
- description: "Makes approval decisions based on review results and configured thresholds"
- );
-
- public static AgentDefinition SummaryAgent => new(
- name: "SummaryAgent",
- role: "Technical Writer",
- systemPrompt: """
- You are a technical writer specializing in creating clear, concise documentation.
-
- Your role is to:
- 1. Summarize pull request changes accurately
- 2. Highlight the purpose and impact of changes
- 3. Assess risk levels objectively
- 4. Identify areas needing special testing attention
-
- Keep summaries under 300 words. Use markdown formatting with bullet points for readability.
- """,
- description: "Creates concise summaries of pull request changes"
- );
}
diff --git a/PRAgent/Agents/AgentFactory.cs b/PRAgent/Agents/AgentFactory.cs
new file mode 100644
index 0000000..573a1d1
--- /dev/null
+++ b/PRAgent/Agents/AgentFactory.cs
@@ -0,0 +1,119 @@
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using PRAgent.Services;
+
+namespace PRAgent.Agents;
+
+///
+/// Semantic Kernel ChatCompletionAgentの作成を集中管理するファクトリクラス
+///
+public class PRAgentFactory
+{
+ private readonly IKernelService _kernelService;
+ private readonly IGitHubService _gitHubService;
+ private readonly PullRequestDataService _prDataService;
+
+ public PRAgentFactory(
+ IKernelService kernelService,
+ IGitHubService gitHubService,
+ PullRequestDataService prDataService)
+ {
+ _kernelService = kernelService;
+ _gitHubService = gitHubService;
+ _prDataService = prDataService;
+ }
+
+ ///
+ /// Reviewエージェントを作成
+ ///
+ public async Task CreateReviewAgentAsync(
+ string owner,
+ string repo,
+ int prNumber,
+ string? customSystemPrompt = null,
+ IEnumerable? functions = null)
+ {
+ var kernel = _kernelService.CreateAgentKernel(AgentDefinition.ReviewAgent.SystemPrompt);
+
+ if (functions != null)
+ {
+ foreach (var function in functions)
+ {
+ kernel.ImportPluginFromObject(function);
+ }
+ }
+
+ var agent = new ChatCompletionAgent
+ {
+ Name = AgentDefinition.ReviewAgent.Name,
+ Description = AgentDefinition.ReviewAgent.Description,
+ Instructions = customSystemPrompt ?? AgentDefinition.ReviewAgent.SystemPrompt,
+ Kernel = kernel
+ };
+
+ return await Task.FromResult(agent);
+ }
+
+ ///
+ /// Reviewエージェント用のKernelを作成(FunctionCalling有効)
+ ///
+ public Kernel CreateReviewKernel(
+ string owner,
+ string repo,
+ int prNumber,
+ string? customSystemPrompt = null)
+ {
+ return _kernelService.CreateAgentKernel(
+ customSystemPrompt ?? AgentDefinition.ReviewAgent.SystemPrompt);
+ }
+
+ ///
+ /// Approvalエージェント用のKernelを作成(後方互換性のため残す)
+ ///
+ [Obsolete("Use CreateReviewKernel instead. Approval functionality is now integrated into ReviewAgent.")]
+ public Kernel CreateApprovalKernel(
+ string owner,
+ string repo,
+ int prNumber,
+ string? customSystemPrompt = null)
+ {
+ return CreateReviewKernel(owner, repo, prNumber, customSystemPrompt);
+ }
+
+ ///
+ /// カスタムエージェントを作成(汎用メソッド)
+ ///
+ public async Task CreateCustomAgentAsync(
+ string name,
+ string description,
+ string systemPrompt,
+ string owner,
+ string repo,
+ int prNumber,
+ IEnumerable? functions = null,
+ KernelArguments? arguments = null)
+ {
+ var kernel = _kernelService.CreateAgentKernel(systemPrompt);
+
+ if (functions != null)
+ {
+ foreach (var function in functions)
+ {
+ kernel.ImportPluginFromObject(function);
+ }
+ }
+
+ var agent = new ChatCompletionAgent
+ {
+ Name = name,
+ Description = description,
+ Instructions = systemPrompt,
+ Kernel = kernel,
+ Arguments = arguments
+ };
+
+ return await Task.FromResult(agent);
+ }
+}
diff --git a/PRAgent/Agents/ApprovalAgent.cs b/PRAgent/Agents/ApprovalAgent.cs
deleted file mode 100644
index 81323ce..0000000
--- a/PRAgent/Agents/ApprovalAgent.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using Microsoft.SemanticKernel;
-using PRAgent.Models;
-using PRAgent.Services;
-
-namespace PRAgent.Agents;
-
-public class ApprovalAgent : BaseAgent
-{
- public ApprovalAgent(
- IKernelService kernelService,
- IGitHubService gitHubService,
- PullRequestDataService prDataService,
- AISettings aiSettings,
- string? customSystemPrompt = null)
- : base(kernelService, gitHubService, prDataService, aiSettings, AgentDefinition.ApprovalAgent, customSystemPrompt)
- {
- }
-
- public new void SetLanguage(string language) => base.SetLanguage(language);
-
- public async Task<(bool ShouldApprove, string Reasoning, string? Comment)> DecideAsync(
- string owner,
- string repo,
- int prNumber,
- string reviewResult,
- ApprovalThreshold threshold,
- CancellationToken cancellationToken = default)
- {
- var pr = await GitHubService.GetPullRequestAsync(owner, repo, prNumber);
- var thresholdDescription = ApprovalThresholdHelper.GetDescription(threshold);
-
- var prompt = $"""
- Based on the code review below, make an approval decision for this pull request.
-
- ## Pull Request
- - Title: {pr.Title}
- - Author: {pr.User.Login}
-
- ## Code Review Result
- {reviewResult}
-
- ## Approval Threshold
- {thresholdDescription}
-
- Provide your decision in this format:
-
- DECISION: [APPROVE/REJECT]
- REASONING: [Explain why, listing any issues above the threshold]
- CONDITIONS: [Any conditions for merge, or N/A]
- APPROVAL_COMMENT: [Brief comment if approved, or N/A]
-
- Be conservative - when in doubt, reject or request additional review.
- """;
-
- var response = await KernelService.InvokePromptAsStringAsync(CreateKernel(), prompt, cancellationToken);
-
- return ApprovalResponseParser.Parse(response);
- }
-
- public async Task ApproveAsync(
- string owner,
- string repo,
- int prNumber,
- string? comment = null)
- {
- var result = await GitHubService.ApprovePullRequestAsync(owner, repo, prNumber, comment);
- return $"PR approved: {result.HtmlUrl}";
- }
-}
diff --git a/PRAgent/Agents/BaseAgent.cs b/PRAgent/Agents/BaseAgent.cs
deleted file mode 100644
index dacc034..0000000
--- a/PRAgent/Agents/BaseAgent.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Microsoft.SemanticKernel;
-using Octokit;
-using PRAgent.Models;
-using PRAgent.Services;
-
-namespace PRAgent.Agents;
-
-public abstract class BaseAgent
-{
- protected readonly IKernelService KernelService;
- protected readonly IGitHubService GitHubService;
- protected readonly PullRequestDataService PRDataService;
- protected readonly AISettings AISettings;
- protected AgentDefinition Definition;
-
- protected BaseAgent(
- IKernelService kernelService,
- IGitHubService gitHubService,
- PullRequestDataService prDataService,
- AISettings aiSettings,
- AgentDefinition definition,
- string? customSystemPrompt = null)
- {
- KernelService = kernelService;
- GitHubService = gitHubService;
- PRDataService = prDataService;
- AISettings = aiSettings;
-
- // Apply language setting to the agent definition
- Definition = definition.WithLanguage(aiSettings.Language);
-
- if (!string.IsNullOrEmpty(customSystemPrompt))
- {
- Definition = new AgentDefinition(
- Definition.Name,
- Definition.Role,
- customSystemPrompt,
- Definition.Description
- );
- }
- }
-
- ///
- /// Sets the language for AI responses dynamically
- ///
- protected void SetLanguage(string language)
- {
- Definition = Definition.WithLanguage(language);
- }
-
- protected Kernel CreateKernel()
- {
- return KernelService.CreateKernel(Definition.SystemPrompt);
- }
-
- protected async Task<(PullRequest pr, IReadOnlyList files, string diff)> GetPRDataAsync(
- string owner, string repo, int prNumber)
- {
- return await PRDataService.GetPullRequestDataAsync(owner, repo, prNumber);
- }
-}
diff --git a/PRAgent/Agents/DetailedCommentAgent.cs b/PRAgent/Agents/DetailedCommentAgent.cs
index c975fec..416080d 100644
--- a/PRAgent/Agents/DetailedCommentAgent.cs
+++ b/PRAgent/Agents/DetailedCommentAgent.cs
@@ -1,210 +1,289 @@
-using Microsoft.SemanticKernel;
using Microsoft.Extensions.Logging;
-using Octokit;
-using PRAgent.Models;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
using PRAgent.Services;
namespace PRAgent.Agents;
///
/// 詳細な行コメントを作成するサブエージェント
-/// Tool呼び出しベースで各問題点ごとにコメントを作成
+/// ReviewAgentからFunction Callingで呼び出され、個々の問題について詳細なコメントを生成
///
-public class DetailedCommentAgent : ReviewAgentBase, IDetailedCommentAgent
+public class DetailedCommentAgent : IDetailedCommentAgent
{
+ private readonly IKernelService _kernelService;
private readonly ILogger _logger;
+ private string _language = "en";
public DetailedCommentAgent(
IKernelService kernelService,
- IGitHubService gitHubService,
- PullRequestDataService prDataService,
- AISettings aiSettings,
- ILogger logger,
- string? customSystemPrompt = null)
- : base(kernelService, gitHubService, prDataService, aiSettings, AgentDefinition.DetailedCommentAgent, customSystemPrompt)
+ ILogger logger)
{
+ _kernelService = kernelService;
_logger = logger;
}
///
/// 言語を動的に設定
///
- public new void SetLanguage(string language) => base.SetLanguage(language);
+ public void SetLanguage(string language)
+ {
+ _language = language;
+ }
///
- /// レビュー結果から詳細な行コメントを作成
+ /// 個々の問題について詳細なコメントを生成(Function Calling用)
///
- public async Task> CreateCommentsAsync(string review, string language)
+ /// ファイルパス
+ /// 行番号
+ /// 周辺コード
+ /// 問題の概要
+ /// 詳細なコメント(JSON形式)
+ [KernelFunction("get_detailed_comment")]
+ public async Task GetDetailedCommentAsync(
+ string filePath,
+ int lineNumber,
+ string codeSnippet,
+ string issueSummary)
{
- SetLanguage(language);
+ var systemPrompt = GetDetailedCommentPrompt(_language);
+ var kernel = _kernelService.CreateAgentKernel(systemPrompt);
- // レビュー内容から問題点を抽出
- var issues = ExtractIssuesFromReview(review);
+ var prompt = $$"""
+ 以下のコードの問題点について、GitHubのプルリクエストレビュー用の詳細なコメントを作成してください。
- // 各問題点に対してTool呼び出しでコメントを作成
- var comments = new List();
+ ## ファイル情報
+ - ファイルパス: {{filePath}}
+ - 行番号: {{lineNumber}}
- foreach (var issue in issues)
- {
- var comment = await CreateCommentForIssueAsync(issue, language);
- if (comment != null)
+ ## 対象コード
+ ```
+ {{codeSnippet}}
+ ```
+
+ ## 問題の概要
+ {{issueSummary}}
+
+ ## 出力形式(JSON)
+ 以下の形式で出力してください:
+ ```json
{
- comments.Add(comment);
+ "path": "{{filePath}}",
+ "line": {{lineNumber}},
+ "body": "詳細なコメント本文",
+ "suggestion": "修正提案のコード(あれば)"
}
- }
+ ```
- return comments;
+ 重要: 必ず有効なJSONオブジェクトのみを出力してください。
+ """;
+
+ var chatService = kernel.GetRequiredService();
+ var chatHistory = new ChatHistory();
+ chatHistory.AddUserMessage(prompt);
+
+ var response = await chatService.GetChatMessageContentsAsync(chatHistory, executionSettings: null, kernel);
+ var responseText = response.FirstOrDefault()?.Content ?? "{}";
+
+ _logger.LogInformation("=== DetailedCommentAgent Response for {File}:{Line} ===\n{Response}",
+ filePath, lineNumber, responseText);
+
+ return responseText;
}
///
- /// レビューから問題点を抽出
+ /// 複数の問題について一括で詳細コメントを生成(バッチ処理用)
///
- private List ExtractIssuesFromReview(string review)
+ public async Task> CreateDetailedCommentsAsync(
+ List issues,
+ string language)
{
- var issues = new List();
-
- // セクションごとに分割
- var sections = review.Split(new[] { "\n\n### ", "\n## ", "\n##\n\n" }, StringSplitOptions.RemoveEmptyEntries);
+ SetLanguage(language);
+ var results = new List();
- foreach (var section in sections)
+ foreach (var issue in issues)
{
- // セクションタイトルを解析
- var titleMatch = System.Text.RegularExpressions.Regex.Match(section, @"^(###\s*\[([A-Z]+)\])?\s*(.+)");
- if (titleMatch.Success)
+ try
{
- var levelStr = titleMatch.Groups[2].Value;
- var title = titleMatch.Groups[3].Value.Trim();
+ var jsonResult = await GetDetailedCommentAsync(
+ issue.FilePath,
+ issue.LineNumber,
+ issue.CodeSnippet,
+ issue.IssueSummary);
- // レベルを判定
- var level = levelStr switch
+ var comment = ParseSingleCommentFromJson(jsonResult);
+ if (comment != null)
{
- "CRITICAL" => Severity.Critical,
- "MAJOR" => Severity.Major,
- "MINOR" => Severity.Minor,
- "POSITIVE" => Severity.Positive,
- _ => Severity.Major
- };
-
- // ファイルパスを抽出
- var pathMatch = System.Text.RegularExpressions.Regex.Match(section, @"\*\*ファイル:\*\*`([^`]+)`");
- var path = pathMatch.Success ? pathMatch.Groups[1].Value : "src/File.cs";
-
- // 行番号を抽出
- var lineMatch = System.Text.RegularExpressions.Regex.Match(section, @"\(lines?\s*(\d+)(?:-(\d+))?\)");
- int? startLine = null;
- int? endLine = null;
-
- if (lineMatch.Success)
- {
- startLine = int.Parse(lineMatch.Groups[1].Value);
- if (lineMatch.Groups[2].Success)
- {
- endLine = int.Parse(lineMatch.Groups[2].Value);
- }
+ results.Add(comment);
}
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create detailed comment for {File}:{Line}",
+ issue.FilePath, issue.LineNumber);
+ }
+ }
- // 問題説明を抽出
- var problemSection = System.Text.RegularExpressions.Regex.Split(section, @"\n\*\*\w+:\*\*");
- var problem = problemSection.Length > 1 ? problemSection[1].Split('\n')[0].Trim() : section.Split('\n')[0].Trim();
+ return results;
+ }
- // 修正提案を抽出
- var suggestion = "";
- var suggestionMatch = System.Text.RegularExpressions.Regex.Match(section, @"```suggestion\s*\n?([^\n`]+)");
- if (suggestionMatch.Success)
- {
- suggestion = suggestionMatch.Groups[1].Value.Trim();
- }
+ ///
+ /// レビュー概要から詳細な行コメントを作成(従来のメソッド、互換性のため残す)
+ ///
+ public async Task> CreateCommentsAsync(string reviewOverview, string language)
+ {
+ SetLanguage(language);
- issues.Add(new ReviewIssue
- {
- Title = title,
- Level = level,
- FilePath = path,
- StartLine = startLine ?? 1,
- EndLine = endLine ?? 1,
- Description = problem,
- Suggestion = suggestion
- });
- }
- }
+ var systemPrompt = GetDetailedCommentPrompt(_language);
+ var kernel = _kernelService.CreateAgentKernel(systemPrompt);
+
+ var prompt = $$"""
+ 以下のレビュー概要に基づいて、GitHubのプルリクエストレビュー用の詳細な行コメントを作成してください。
+
+ ## レビュー概要
+ {{reviewOverview}}
+
+ ## 指示
+ 1. 各問題点に対して、ファイルパス、行番号、コメント本文を抽出
+ 2. コメントは簡潔かつ建設的に
+ 3. 修正提案がある場合は suggestion ブロックを含める
+
+ ## 出力形式(JSON)
+ ```json
+ [
+ {
+ "path": "src/File.cs",
+ "line": 45,
+ "body": "ここでメモリリークが発生する可能性があります。using文を使用してください。",
+ "suggestion": "using var resource = ...;"
+ }
+ ]
+ ```
+
+ 重要: 必ず有効なJSON配列のみを出力してください。
+ """;
+
+ var chatService = kernel.GetRequiredService();
+ var chatHistory = new ChatHistory();
+ chatHistory.AddUserMessage(prompt);
- return issues;
+ var response = await chatService.GetChatMessageContentsAsync(chatHistory, executionSettings: null, kernel);
+ var responseText = response.FirstOrDefault()?.Content ?? "[]";
+
+ _logger.LogInformation("=== DetailedCommentAgent Response ===\n{Response}", responseText);
+
+ return ParseCommentsFromJson(responseText);
}
///
- /// 各問題点に対してコメントを作成
+ /// 単一のJSONオブジェクトから行コメントデータをパース
///
- private async Task CreateCommentForIssueAsync(ReviewIssue issue, string language)
+ private LineCommentData? ParseSingleCommentFromJson(string json)
{
try
{
- var prompt = CreateCommentPrompt(issue, language);
+ // コードブロック内のJSONを探す
+ var codeBlockMatch = System.Text.RegularExpressions.Regex.Match(json, @"```(?:json)?\s*([\s\S]*?)```");
+ if (codeBlockMatch.Success)
+ {
+ json = codeBlockMatch.Groups[1].Value.Trim();
+ }
+
+ // JSONオブジェクトを探す
+ var jsonMatch = System.Text.RegularExpressions.Regex.Match(json, @"\{[\s\S]*?\}");
+ if (!jsonMatch.Success)
+ {
+ _logger.LogWarning("No valid JSON object found in response");
+ return null;
+ }
+
+ return System.Text.Json.JsonSerializer.Deserialize(jsonMatch.Value);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to parse single comment from JSON");
+ return null;
+ }
+ }
- // プロンプットを出力
- _logger.LogInformation("=== DetailedCommentAgent Prompt for Issue ===\n{Prompt}", prompt);
+ ///
+ /// JSON配列から行コメントデータをパース
+ ///
+ private List ParseCommentsFromJson(string json)
+ {
+ try
+ {
+ // まずコードブロック内のJSONを探す
+ var codeBlockMatch = System.Text.RegularExpressions.Regex.Match(json, @"```(?:json)?\s*([\s\S]*?)```");
+ if (codeBlockMatch.Success)
+ {
+ json = codeBlockMatch.Groups[1].Value.Trim();
+ }
- var aiResponse = await KernelService.InvokePromptAsStringAsync(CreateKernel(), prompt);
+ // JSON配列を探す(非貪欲マッチ)
+ var jsonMatch = System.Text.RegularExpressions.Regex.Match(json, @"\[[\s\S]*?\]");
+ if (!jsonMatch.Success)
+ {
+ _logger.LogWarning("No valid JSON array found in response");
+ return new List();
+ }
- _logger.LogInformation("=== DetailedCommentAgent Response ===\n{Response}", aiResponse);
+ var jsonArray = jsonMatch.Value;
+ var comments = System.Text.Json.JsonSerializer.Deserialize>(jsonArray);
- return new DraftPullRequestReviewComment(issue.FilePath, aiResponse, issue.StartLine);
+ return comments ?? new List();
}
- catch
+ catch (Exception ex)
{
- // フォールバック:簡潔なコメントを作成
- return new DraftPullRequestReviewComment(
- issue.FilePath,
- $"{issue.Level}: {issue.Description}\n\n{issue.Suggestion}",
- issue.StartLine);
+ _logger.LogError(ex, "Failed to parse comments from JSON");
+ return new List();
}
}
///
- /// コメント生成用のプロンプトを作成
+ /// 言語に応じた詳細コメント用プロンプトを取得
///
- private string CreateCommentPrompt(ReviewIssue issue, string language)
+ private static string GetDetailedCommentPrompt(string language)
{
- return $$"""
- Create a detailed GitHub pull request review comment for this issue:
-
- **Issue Title:** {{issue.Title}}
- **Level:** {{issue.Level}}
- **File:** {{issue.FilePath}} (Line {{issue.StartLine}})
- **Description:** {{issue.Description}}
- **Suggestion:** {{issue.Suggestion}}
-
- Create a concise comment that:
- 1. Clearly describes the problem
- 2. Provides actionable feedback
- 3. Uses professional and constructive language
- 4. Keep it under 200 words
-
- **Output:** Only the comment text, no formatting.
- """;
- }
-}
+ var isJapanese = language?.ToLowerInvariant() == "ja";
-///
-/// レビュー問題のデータモデル
-///
-public class ReviewIssue
-{
- public string Title { get; set; } = string.Empty;
- public Severity Level { get; set; }
- public string FilePath { get; set; } = string.Empty;
- public int StartLine { get; set; }
- public int EndLine { get; set; }
- public string Description { get; set; } = string.Empty;
- public string Suggestion { get; set; } = string.Empty;
-}
+ if (isJapanese)
+ {
+ return """
+ あなたはGitHubのプルリクエストレビュー用の詳細コメントを作成する専門家です。
-///
-/// レベルの列挙型
-///
-public enum Severity
-{
- Critical,
- Major,
- Minor,
- Positive
-}
\ No newline at end of file
+ ## 役割
+ 指定されたコードの問題点について、詳細なコメントを生成
+
+ ## 出力ルール
+ 1. 有効なJSONオブジェクトのみを出力
+ 2. コメントには以下を含める:
+ - path: ファイルパス(入力と同じ値)
+ - line: 行番号(入力と同じ値)
+ - body: 詳細なコメント本文
+ - suggestion: 修正提案(オプション)
+ 3. コメントは建設的で、修正方法を具体的に提示
+ 4. コードスニペットを参照して、具体的な改善案を提示
+ """;
+ }
+ else
+ {
+ return """
+ You are an expert at creating detailed line comments for GitHub pull request reviews.
+
+ ## Your Role
+ Generate detailed comments for specified code issues.
+
+ ## Output Rules
+ 1. Output only valid JSON object
+ 2. Include in comment:
+ - path: file path (same as input)
+ - line: line number (same as input)
+ - body: detailed comment body
+ - suggestion: fix suggestion (optional)
+ 3. Keep comments constructive with specific improvement suggestions
+ 4. Reference the code snippet and provide concrete fixes
+ """;
+ }
+ }
+}
diff --git a/PRAgent/Agents/ReviewAgent.cs b/PRAgent/Agents/ReviewAgent.cs
deleted file mode 100644
index 21ccd19..0000000
--- a/PRAgent/Agents/ReviewAgent.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using Microsoft.SemanticKernel;
-using Microsoft.Extensions.Logging;
-using Octokit;
-using PRAgent.Models;
-using PRAgent.Services;
-
-namespace PRAgent.Agents;
-
-public class ReviewAgent : ReviewAgentBase
-{
- private readonly IDetailedCommentAgent _detailedCommentAgent;
- private readonly ILogger _logger;
-
- public ReviewAgent(
- IKernelService kernelService,
- IGitHubService gitHubService,
- PullRequestDataService prDataService,
- AISettings aiSettings,
- IDetailedCommentAgent detailedCommentAgent,
- ILogger logger,
- string? customSystemPrompt = null)
- : base(kernelService, gitHubService, prDataService, aiSettings, AgentDefinition.ReviewAgent, customSystemPrompt)
- {
- _detailedCommentAgent = detailedCommentAgent;
- _logger = logger;
- }
-
- public new void SetLanguage(string language) => base.SetLanguage(language);
-
- public async Task ReviewAsync(
- string owner,
- string repo,
- int prNumber,
- CancellationToken cancellationToken = default)
- {
- var (pr, files, diff) = await GetPRDataAsync(owner, repo, prNumber);
- var fileList = PullRequestDataService.FormatFileList(files);
-
- var systemPrompt = """
- You are an expert code reviewer with deep knowledge of software engineering best practices.
- Your role is to provide thorough, constructive, and actionable feedback on pull requests.
-
- When reviewing code, focus on:
- - Correctness and potential bugs
- - Security vulnerabilities
- - Performance considerations
- - Code organization and readability
- - Adherence to best practices and design patterns
- - Test coverage and quality
-
- **Output Format:**
- - Organize findings by individual issue/problem
- - Each issue should be a separate section starting with ###
- - Use severity labels: [CRITICAL], [MAJOR], [MINOR], [POSITIVE]
- - For each issue, include:
- * File path (extract from review or use a reasonable default)
- * Line numbers (if applicable)
- * Detailed description
- * Specific code examples
- * Concrete suggestions
-
- Example:
- ### [CRITICAL] SQL Injection Vulnerability
-
- **File:** `src/Authentication.cs` (lines 45-52)
-
- **Problem:** User input is directly concatenated into SQL queries...
-
- **Suggested Fix:**
- ```suggestion
- // Use parameterized queries
- var query = "SELECT * FROM Users WHERE Id = @Id";
- ```
- """;
-
- var prompt = PullRequestDataService.CreateReviewPrompt(pr, fileList, diff, systemPrompt);
-
- // プロンプットを出力
- _logger.LogInformation("=== ReviewAgent Prompt ===\n{Prompt}", prompt);
-
- var review = await KernelService.InvokePromptAsStringAsync(CreateKernel(), prompt, cancellationToken);
-
- _logger.LogInformation("=== ReviewAgent Response ===\n{Response}", review);
-
- // サブエージェントに渡すために言語設定
- _detailedCommentAgent.SetLanguage(AISettings.Language);
-
- // サブエージェントで詳細なコメントを作成
- var comments = await _detailedCommentAgent.CreateCommentsAsync(review, AISettings.Language);
-
- return review;
- }
-}
\ No newline at end of file
diff --git a/PRAgent/Agents/SK/SKReviewAgent.cs b/PRAgent/Agents/SK/SKReviewAgent.cs
new file mode 100644
index 0000000..06789cd
--- /dev/null
+++ b/PRAgent/Agents/SK/SKReviewAgent.cs
@@ -0,0 +1,496 @@
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using PRAgent.Models;
+using PRAgent.Services;
+using PRAgent.Plugins.GitHub;
+using PRAgentDefinition = PRAgent.Agents.AgentDefinition;
+
+namespace PRAgent.Agents.SK;
+
+///
+/// Semantic Kernel ChatCompletionAgentベースのレビューエージェント
+/// ReviewAgent(概要)とDetailedCommentAgent(詳細)を連携させ、一つのReviewとして投稿
+///
+public class SKReviewAgent
+{
+ private readonly PRAgentFactory _agentFactory;
+ private readonly PullRequestDataService _prDataService;
+ private readonly IGitHubService _gitHubService;
+ private readonly IDetailedCommentAgent _detailedCommentAgent;
+
+ public SKReviewAgent(
+ PRAgentFactory agentFactory,
+ PullRequestDataService prDataService,
+ IGitHubService gitHubService,
+ IDetailedCommentAgent detailedCommentAgent)
+ {
+ _agentFactory = agentFactory;
+ _prDataService = prDataService;
+ _gitHubService = gitHubService;
+ _detailedCommentAgent = detailedCommentAgent;
+ }
+
+ ///
+ /// プルリクエストのコードレビューを実行します
+ ///
+ public async Task ReviewAsync(
+ string owner,
+ string repo,
+ int prNumber,
+ string? customSystemPrompt = null,
+ CancellationToken cancellationToken = default)
+ {
+ // Reviewエージェントを作成
+ var agent = await _agentFactory.CreateReviewAgentAsync(owner, repo, prNumber, customSystemPrompt);
+
+ // PRデータを取得
+ var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber);
+ var fileList = PullRequestDataService.FormatFileList(files);
+
+ // プロンプトを作成
+ var systemPrompt = customSystemPrompt ?? PRAgentDefinition.ReviewAgent.SystemPrompt;
+ var prompt = PullRequestDataService.CreateReviewPrompt(pr, fileList, diff, systemPrompt);
+
+ // チャット履歴を作成してエージェントを実行
+ var chatHistory = new ChatHistory();
+ chatHistory.AddUserMessage(prompt);
+
+ var responses = new System.Text.StringBuilder();
+ await foreach (var response in agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken))
+ {
+ responses.Append(response.Message.Content);
+ }
+
+ return responses.ToString();
+ }
+
+ ///
+ /// ストリーミングでコードレビューを実行します
+ ///
+ public async IAsyncEnumerable ReviewStreamingAsync(
+ string owner,
+ string repo,
+ int prNumber,
+ string? customSystemPrompt = null,
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ // Reviewエージェントを作成
+ var agent = await _agentFactory.CreateReviewAgentAsync(owner, repo, prNumber, customSystemPrompt);
+
+ // PRデータを取得
+ var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber);
+ var fileList = PullRequestDataService.FormatFileList(files);
+
+ // プロンプトを作成
+ var systemPrompt = customSystemPrompt ?? PRAgentDefinition.ReviewAgent.SystemPrompt;
+ var prompt = PullRequestDataService.CreateReviewPrompt(pr, fileList, diff, systemPrompt);
+
+ // チャット履歴を作成してエージェントを実行
+ var chatHistory = new ChatHistory();
+ chatHistory.AddUserMessage(prompt);
+
+ await foreach (var response in agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken))
+ {
+ yield return response.Message.Content ?? string.Empty;
+ }
+ }
+
+ ///
+ /// ReviewAgent(概要)とDetailedCommentAgent(詳細)を連携させたレビューを実行
+ /// Function CallingでDetailedCommentAgentを呼び出し
+ ///
+ public async Task<(string ReviewText, PRActionResult? ActionResult)> ReviewWithLineCommentsAsync(
+ string owner,
+ string repo,
+ int prNumber,
+ string? language = null,
+ CancellationToken cancellationToken = default)
+ {
+ // PRデータを取得
+ var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber);
+ var fileList = PullRequestDataService.FormatFileList(files);
+
+ // ===== バッファとKernelを準備 =====
+ var buffer = new PRActionBuffer();
+ _detailedCommentAgent.SetLanguage(language ?? "en");
+
+ var reviewSystemPrompt = GetReviewWithSubAgentPrompt(language);
+
+ // Kernelを作成してツールを登録
+ var kernel = _agentFactory.CreateReviewKernel(owner, repo, prNumber, reviewSystemPrompt);
+
+ // ツールを登録
+ var commentPlugin = new PostCommentFunction(buffer);
+ var approvePlugin = new ApprovePRFunction(buffer);
+ kernel.Plugins.AddFromObject(commentPlugin, "pr_actions");
+ kernel.Plugins.AddFromObject(approvePlugin, "pr_actions");
+ kernel.Plugins.AddFromObject(_detailedCommentAgent, "sub_agents");
+
+ // Function Calling設定
+ var executionSettings = new OpenAIPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
+ };
+
+ // ===== ReviewAgentでレビュー実行(Function CallingでSubAgent呼び出し) =====
+ var reviewPrompt = $"""
+ 以下のプルリクエストのコードレビューを行ってください。
+
+ ## プルリクエスト情報
+ - タイトル: {pr.Title}
+ - 作成者: {pr.User.Login}
+ - 説明: {pr.Body}
+
+ ## 変更されたファイル
+ {fileList}
+
+ ## 差分
+ {diff}
+
+ ## レビュー指示
+ 利用可能なツールを使ってレビューを行ってください:
+
+ 1. **post_review_comment**: レビューの概要を投稿
+ - 全体的な概要(3-5行程度)
+ - レビューのまとめ
+
+ 2. **get_detailed_comment**: 個々の問題について詳細なコメントを生成
+ - ファイルパス、行番号、周辺コード、問題の概要を指定
+ - SubAgentが詳細なコメントと修正提案を生成
+ - 各問題に対して呼び出す
+
+ 3. **post_line_comment**: 生成された詳細コメントを投稿
+ - get_detailed_commentの結果を使って投稿
+
+ 4. **approve_pull_request**: PRを承認
+ - Criticalな問題がない場合に使用
+
+ 5. **request_changes**: 変更を依頼
+ - Criticalな問題がある場合に使用
+
+ ## ワークフロー
+ 1. まず post_review_comment で概要を投稿
+ 2. 各問題について:
+ a. get_detailed_comment で詳細なコメントを生成
+ b. post_line_comment でコメントを投稿
+ 3. 最後に approve_pull_request または request_changes で判定
+ """;
+
+ var chatService = kernel.GetRequiredService();
+ var chatHistory = new ChatHistory();
+ chatHistory.AddUserMessage(reviewPrompt);
+
+ var reviewResponses = new System.Text.StringBuilder();
+ await foreach (var content in chatService.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken))
+ {
+ reviewResponses.Append(content.Content);
+ }
+
+ var reviewText = reviewResponses.ToString();
+
+ // ===== アクションを実行 =====
+ PRActionResult? actionResult = null;
+ var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber);
+ var state = buffer.GetState();
+
+ if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || state.ApprovalState != PRApprovalState.None)
+ {
+ actionResult = await executor.ExecuteAsync(buffer, cancellationToken);
+ }
+
+ return (reviewText, actionResult);
+ }
+
+ ///
+ /// 言語に応じた概要レビュープロンプトを取得します
+ ///
+ private static string GetReviewOverviewPrompt(string? language)
+ {
+ var isJapanese = language?.ToLowerInvariant() == "ja";
+
+ if (isJapanese)
+ {
+ return """
+ あなたはシニアソフトウェアエンジニアとしてプルリクエストのコードレビューを行います。
+
+ ## 役割
+ 1. コードの全体的な概要を作成(3-5行程度)
+ 2. 発見した問題点をリストアップ
+ 3. 各問題には重要度を付与(Critical/Major/Minor)
+
+ ## 出力形式
+ ## 概要
+ [全体的な概要]
+
+ ## 発見した問題
+ ### [Critical/Major/Minor] 問題タイトル
+ **ファイル:** `path/to/file.cs`
+ **行番号:** 45
+ **説明:** 問題の詳細説明
+ **修正提案:**
+ ```suggestion
+ // 修正内容
+ ```
+
+ 簡潔で建設的なフィードバックを心がけてください。
+ """;
+ }
+ else
+ {
+ return """
+ You are a senior software engineer performing code reviews on pull requests.
+
+ ## Your Role
+ 1. Create a brief overview (3-5 lines)
+ 2. List all issues found with severity (Critical/Major/Minor)
+ 3. For each issue, include file path, line number, description, and suggestion
+
+ ## Output Format
+ ## Overview
+ [Brief overview of the changes]
+
+ ## Issues Found
+ ### [Critical/Major/Minor] Issue Title
+ **File:** `path/to/file.cs`
+ **Line:** 45
+ **Description:** Detailed explanation of the issue
+ **Suggestion:**
+ ```suggestion
+ // Fixed code
+ ```
+
+ Keep feedback concise and constructive.
+ """;
+ }
+ }
+
+ ///
+ /// SubAgentありモード用のプロンプトを取得
+ ///
+ private static string GetReviewWithSubAgentPrompt(string? language)
+ {
+ var isJapanese = language?.ToLowerInvariant() == "ja";
+
+ if (isJapanese)
+ {
+ return """
+ あなたはシニアソフトウェアエンジニアとしてプルリクエストのコードレビューを行います。
+
+ ## 役割
+ 提供されたツールを使ってレビューを行い、詳細なコメントはSubAgentに委任します。
+
+ ## 利用可能なツール
+ - post_review_comment: レビューの概要を投稿
+ - get_detailed_comment: SubAgentを呼び出して詳細なコメントを生成
+ - post_line_comment: 生成された詳細コメントを投稿
+ - approve_pull_request: PRを承認
+ - request_changes: 変更を依頼
+
+ ## レビューの基準
+ - Critical: セキュリティ脆弱性、バグ、データ損失の可能性
+ - Major: パフォーマンス問題、保守性の問題
+ - Minor: コードスタイル、軽微な改善提案
+
+ ## ワークフロー
+ 1. コードを分析して問題を特定
+ 2. post_review_commentで概要を投稿
+ 3. 各問題について:
+ a. get_detailed_commentでSubAgentに詳細コメントを生成させる
+ b. post_line_commentでコメントを投稿
+ 4. Criticalな問題がなければapprove、あればrequest_changes
+
+ 重要: 行コメントの内容は必ず get_detailed_comment を使ってSubAgentに生成させてください。
+ """;
+ }
+ else
+ {
+ return """
+ You are a senior software engineer performing code reviews on pull requests.
+
+ ## Your Role
+ Review code using provided tools, delegating detailed comments to SubAgent.
+
+ ## Available Tools
+ - post_review_comment: Post review overview
+ - get_detailed_comment: Call SubAgent to generate detailed comment
+ - post_line_comment: Post the generated detailed comment
+ - approve_pull_request: Approve the PR
+ - request_changes: Request changes
+
+ ## Review Criteria
+ - Critical: Security vulnerabilities, bugs, data loss potential
+ - Major: Performance issues, maintainability problems
+ - Minor: Code style, minor improvements
+
+ ## Workflow
+ 1. Analyze code and identify issues
+ 2. Post overview with post_review_comment
+ 3. For each issue:
+ a. Use get_detailed_comment to have SubAgent generate detailed comment
+ b. Post comment with post_line_comment
+ 4. approve if no Critical issues, request_changes if any
+
+ Important: Always use get_detailed_comment to have SubAgent generate line comment content.
+ """;
+ }
+ }
+ public async Task<(string ReviewText, PRActionResult? ActionResult)> ReviewDirectAsync(
+ string owner,
+ string repo,
+ int prNumber,
+ string? language = null,
+ CancellationToken cancellationToken = default)
+ {
+ // PRデータを取得
+ var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber);
+ var fileList = PullRequestDataService.FormatFileList(files);
+
+ // ===== バッファとKernelを準備 =====
+ var buffer = new PRActionBuffer();
+ var systemPrompt = GetDirectReviewPrompt(language);
+
+ // Kernelを作成してツールを登録
+ var kernel = _agentFactory.CreateReviewKernel(owner, repo, prNumber, systemPrompt);
+
+ // すべてのツールを登録
+ var commentPlugin = new PostCommentFunction(buffer);
+ var approvePlugin = new ApprovePRFunction(buffer);
+ kernel.Plugins.AddFromObject(commentPlugin, "pr_actions");
+ kernel.Plugins.AddFromObject(approvePlugin, "pr_actions");
+
+ // Function Calling設定
+ var executionSettings = new OpenAIPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
+ };
+
+ // ===== レビュープロンプト =====
+ var reviewPrompt = $"""
+ 以下のプルリクエストのコードレビューを行ってください。
+
+ ## プルリクエスト情報
+ - タイトル: {pr.Title}
+ - 作成者: {pr.User.Login}
+ - 説明: {pr.Body}
+
+ ## 変更されたファイル
+ {fileList}
+
+ ## 差分
+ {diff}
+
+ ## レビュー指示
+ 利用可能なツールを使ってレビューを行ってください:
+
+ 1. **post_review_comment**: レビューの概要を投稿(必須)
+ - 全体的な概要(3-5行程度)
+ - レビューのまとめ
+
+ 2. **post_line_comment**: 特定の行にコメントを投稿
+ - ファイルパス、行番号、コメント内容を指定
+ - 修正提案がある場合はsuggestionパラメータを使用
+
+ 3. **approve_pull_request**: PRを承認
+ - Criticalな問題がない場合に使用
+
+ 4. **request_changes**: 変更を依頼
+ - Criticalな問題がある場合に使用
+
+ ## ワークフロー
+ 1. まず post_review_comment で概要を投稿
+ 2. 問題がある行に対して post_line_comment でコメント
+ 3. 最後に approve_pull_request または request_changes で判定
+ """;
+
+ var chatService = kernel.GetRequiredService();
+ var chatHistory = new ChatHistory();
+ chatHistory.AddUserMessage(reviewPrompt);
+
+ var reviewResponses = new System.Text.StringBuilder();
+ await foreach (var content in chatService.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken))
+ {
+ reviewResponses.Append(content.Content);
+ }
+
+ var reviewText = reviewResponses.ToString();
+
+ // ===== アクションを実行 =====
+ PRActionResult? actionResult = null;
+ var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber);
+ var state = buffer.GetState();
+
+ if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || state.ApprovalState != PRApprovalState.None)
+ {
+ actionResult = await executor.ExecuteAsync(buffer, cancellationToken);
+ }
+
+ return (reviewText, actionResult);
+ }
+
+ ///
+ /// SubAgentなしモード用のプロンプトを取得
+ ///
+ private static string GetDirectReviewPrompt(string? language)
+ {
+ var isJapanese = language?.ToLowerInvariant() == "ja";
+
+ if (isJapanese)
+ {
+ return """
+ あなたはシニアソフトウェアエンジニアとしてプルリクエストのコードレビューを行います。
+
+ ## 役割
+ 提供されたツールを使って、プルリクエストのレビューを行ってください。
+
+ ## 利用可能なツール
+ - post_review_comment: レビューの概要を投稿
+ - post_line_comment: 特定の行にコメントを投稿
+ - approve_pull_request: PRを承認
+ - request_changes: 変更を依頼
+
+ ## レビューの基準
+ - Critical: セキュリティ脆弱性、バグ、データ損失の可能性
+ - Major: パフォーマンス問題、保守性の問題
+ - Minor: コードスタイル、軽微な改善提案
+
+ ## ワークフロー
+ 1. コードを分析
+ 2. post_review_commentで概要を投稿
+ 3. 問題がある箇所にpost_line_commentでコメント
+ 4. Criticalな問題がなければapprove、あればrequest_changes
+
+ 簡潔で建設的なフィードバックを心がけてください。
+ """;
+ }
+ else
+ {
+ return """
+ You are a senior software engineer performing code reviews on pull requests.
+
+ ## Your Role
+ Use the provided tools to review pull requests.
+
+ ## Available Tools
+ - post_review_comment: Post review overview
+ - post_line_comment: Post comment on specific line
+ - approve_pull_request: Approve the PR
+ - request_changes: Request changes
+
+ ## Review Criteria
+ - Critical: Security vulnerabilities, bugs, data loss potential
+ - Major: Performance issues, maintainability problems
+ - Minor: Code style, minor improvements
+
+ ## Workflow
+ 1. Analyze the code
+ 2. Post overview with post_review_comment
+ 3. Comment on issues with post_line_comment
+ 4. approve if no Critical issues, request_changes if any
+
+ Keep feedback concise and constructive.
+ """;
+ }
+ }
+}
diff --git a/PRAgent/Agents/SelectionStrategies.cs b/PRAgent/Agents/SelectionStrategies.cs
new file mode 100644
index 0000000..40537d7
--- /dev/null
+++ b/PRAgent/Agents/SelectionStrategies.cs
@@ -0,0 +1,262 @@
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Chat;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace PRAgent.Agents;
+
+///
+/// Semantic Kernel AgentGroupChat用の選択戦略
+///
+public static class SelectionStrategies
+{
+ ///
+ /// シーケンシャル選択戦略 - エージェントを順番に選択
+ ///
+ public class SequentialSelectionStrategy : SelectionStrategy
+ {
+ private int _currentIndex = 0;
+
+ public SequentialSelectionStrategy()
+ {
+ }
+
+ protected override async Task SelectAgentAsync(
+ IReadOnlyList agents,
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default)
+ {
+ // シーケンシャルにエージェントを選択
+ if (agents.Count == 0)
+ {
+ throw new InvalidOperationException("No agents available for selection.");
+ }
+
+ var selectedAgent = agents[_currentIndex];
+ _currentIndex = (_currentIndex + 1) % agents.Count;
+
+ return await Task.FromResult(selectedAgent);
+ }
+ }
+
+ ///
+ /// Approvalワークフロー選択戦略 - Review → Summary → Approval の順に選択
+ ///
+ public class ApprovalWorkflowStrategy : SelectionStrategy
+ {
+ private enum WorkflowStage
+ {
+ Review,
+ Summary,
+ Approval,
+ Complete
+ }
+
+ private WorkflowStage _currentStage = WorkflowStage.Review;
+
+ public ApprovalWorkflowStrategy()
+ {
+ }
+
+ protected override async Task SelectAgentAsync(
+ IReadOnlyList agents,
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default)
+ {
+ // ワークフローの現在ステージに基づいてエージェントを選択
+ Agent? selectedAgent = _currentStage switch
+ {
+ WorkflowStage.Review => agents.FirstOrDefault(a => a.Name == "ReviewAgent"),
+ WorkflowStage.Summary => agents.FirstOrDefault(a => a.Name == "SummaryAgent"),
+ WorkflowStage.Approval => agents.FirstOrDefault(a => a.Name == "ApprovalAgent"),
+ _ => null
+ };
+
+ if (selectedAgent == null)
+ {
+ // 指定した名前のエージェントが見つからない場合は最初のエージェントを使用
+ selectedAgent = agents.FirstOrDefault();
+ if (selectedAgent == null)
+ {
+ throw new InvalidOperationException($"No agent available for stage: {_currentStage}");
+ }
+ }
+
+ // 次のステージに進む
+ _currentStage = _currentStage switch
+ {
+ WorkflowStage.Review => WorkflowStage.Summary,
+ WorkflowStage.Summary => WorkflowStage.Approval,
+ WorkflowStage.Approval => WorkflowStage.Complete,
+ _ => WorkflowStage.Complete
+ };
+
+ // ワークフローが完了したら最初に戻す(必要に応じて)
+ if (_currentStage == WorkflowStage.Complete)
+ {
+ _currentStage = WorkflowStage.Review;
+ }
+
+ return await Task.FromResult(selectedAgent);
+ }
+ }
+
+ ///
+ /// 条件付き選択戦略 - 履歴の内容に基づいて次のエージェントを選択
+ ///
+ public class ConditionalSelectionStrategy : SelectionStrategy
+ {
+ public ConditionalSelectionStrategy()
+ {
+ }
+
+ protected override async Task SelectAgentAsync(
+ IReadOnlyList agents,
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default)
+ {
+ if (agents.Count == 0)
+ {
+ throw new InvalidOperationException("No agents available for selection.");
+ }
+
+ // 最後のメッセージの内容を確認して、次のエージェントを決定
+ Agent? selectedAgent;
+
+ if (history.Count == 0)
+ {
+ // 最初はReviewAgentから
+ selectedAgent = agents.FirstOrDefault(a => a.Name == "ReviewAgent") ?? agents[0];
+ }
+ else
+ {
+ var lastMessage = history[^1];
+ var lastAgentName = lastMessage.AuthorName ?? string.Empty;
+
+ // 前のエージェントに基づいて次のエージェントを選択
+ selectedAgent = lastAgentName switch
+ {
+ "ReviewAgent" => agents.FirstOrDefault(a => a.Name == "SummaryAgent"),
+ "SummaryAgent" => agents.FirstOrDefault(a => a.Name == "ApprovalAgent"),
+ "ApprovalAgent" => agents.FirstOrDefault(a => a.Name == "ReviewAgent"), // ループ
+ _ => agents[0]
+ };
+ }
+
+ return await Task.FromResult(selectedAgent ?? agents[0]);
+ }
+ }
+
+ ///
+ /// ラウンドロビン選択戦略 - エージェントを均等に選択
+ ///
+ public class RoundRobinSelectionStrategy : SelectionStrategy
+ {
+ private readonly Dictionary _agentUsageCount = new();
+
+ public RoundRobinSelectionStrategy()
+ {
+ }
+
+ protected override async Task SelectAgentAsync(
+ IReadOnlyList agents,
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default)
+ {
+ if (agents.Count == 0)
+ {
+ throw new InvalidOperationException("No agents available for selection.");
+ }
+
+ // 初期化
+ foreach (var agent in agents)
+ {
+ if (!_agentUsageCount.ContainsKey(agent.Name ?? string.Empty))
+ {
+ _agentUsageCount[agent.Name ?? string.Empty] = 0;
+ }
+ }
+
+ // 最も使用回数が少ないエージェントを選択
+ var selectedAgent = agents
+ .OrderBy(a => _agentUsageCount.TryGetValue(a.Name ?? string.Empty, out var count) ? count : 0)
+ .First();
+
+ if (selectedAgent.Name != null)
+ {
+ _agentUsageCount[selectedAgent.Name]++;
+ }
+
+ return await Task.FromResult(selectedAgent);
+ }
+ }
+
+ ///
+ /// 最後のメッセージに基づく選択戦略 - メッセージの内容を解析して次のエージェントを選択
+ ///
+ public class ContentBasedSelectionStrategy : SelectionStrategy
+ {
+ public ContentBasedSelectionStrategy()
+ {
+ }
+
+ protected override async Task SelectAgentAsync(
+ IReadOnlyList agents,
+ IReadOnlyList history,
+ CancellationToken cancellationToken = default)
+ {
+ if (agents.Count == 0)
+ {
+ throw new InvalidOperationException("No agents available for selection.");
+ }
+
+ // 最初のメッセージの場合はReviewAgentから
+ if (history.Count == 0)
+ {
+ var reviewAgent = agents.FirstOrDefault(a => a.Name == "ReviewAgent");
+ if (reviewAgent != null)
+ {
+ return await Task.FromResult(reviewAgent);
+ }
+ return await Task.FromResult(agents[0]);
+ }
+
+ // 最後のメッセージの内容を解析
+ var lastMessage = history[^1];
+ var content = lastMessage.Content ?? string.Empty;
+
+ // キーワードに基づいてエージェントを選択
+ Agent? selectedAgent;
+
+ if (content.Contains("DECISION:", StringComparison.OrdinalIgnoreCase) ||
+ content.Contains("approve", StringComparison.OrdinalIgnoreCase))
+ {
+ // 承認決定が必要な場合はReviewAgentに戻るか、次のステップへ
+ selectedAgent = agents.FirstOrDefault(a => a.Name == "ReviewAgent") ?? agents[0];
+ }
+ else if (content.Contains("[CRITICAL]") || content.Contains("[MAJOR]"))
+ {
+ // 重要な問題が見つかった場合はApprovalAgentへ
+ selectedAgent = agents.FirstOrDefault(a => a.Name == "ApprovalAgent") ?? agents[0];
+ }
+ else if (content.Contains("## Summary") || content.Contains("Summary"))
+ {
+ // 要約が含まれている場合はApprovalAgentへ
+ selectedAgent = agents.FirstOrDefault(a => a.Name == "ApprovalAgent") ?? agents[0];
+ }
+ else
+ {
+ // それ以外の場合は次のエージェントへ
+ var lastAgentName = lastMessage.AuthorName ?? string.Empty;
+ selectedAgent = lastAgentName switch
+ {
+ "ReviewAgent" => agents.FirstOrDefault(a => a.Name == "SummaryAgent"),
+ "SummaryAgent" => agents.FirstOrDefault(a => a.Name == "ApprovalAgent"),
+ _ => agents.FirstOrDefault(a => a.Name == "ReviewAgent")
+ };
+ }
+
+ return await Task.FromResult(selectedAgent ?? agents[0]);
+ }
+ }
+}
diff --git a/PRAgent/Agents/SummaryAgent.cs b/PRAgent/Agents/SummaryAgent.cs
deleted file mode 100644
index 6d3fb72..0000000
--- a/PRAgent/Agents/SummaryAgent.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Microsoft.SemanticKernel;
-using PRAgent.Models;
-using PRAgent.Services;
-
-namespace PRAgent.Agents;
-
-public class SummaryAgent : BaseAgent
-{
- public SummaryAgent(
- IKernelService kernelService,
- IGitHubService gitHubService,
- PullRequestDataService prDataService,
- AISettings aiSettings,
- string? customSystemPrompt = null)
- : base(kernelService, gitHubService, prDataService, aiSettings, AgentDefinition.SummaryAgent, customSystemPrompt)
- {
- }
-
- public new void SetLanguage(string language) => base.SetLanguage(language);
-
- public async Task SummarizeAsync(
- string owner,
- string repo,
- int prNumber,
- CancellationToken cancellationToken = default)
- {
- var (pr, files, diff) = await GetPRDataAsync(owner, repo, prNumber);
- var fileList = PullRequestDataService.FormatFileList(files);
-
- var systemPrompt = """
- You are a technical writer specializing in creating clear, concise documentation.
- Your role is to summarize pull request changes accurately.
- """;
-
- var prompt = PullRequestDataService.CreateSummaryPrompt(pr, fileList, diff, systemPrompt);
-
- return await KernelService.InvokePromptAsStringAsync(CreateKernel(), prompt, cancellationToken);
- }
-}
diff --git a/PRAgent/CommandLine/CommandLineOptions.cs b/PRAgent/CommandLine/CommandLineOptions.cs
index 594fda2..f6c66dd 100644
--- a/PRAgent/CommandLine/CommandLineOptions.cs
+++ b/PRAgent/CommandLine/CommandLineOptions.cs
@@ -11,7 +11,7 @@ public abstract record CommandOptions
public string? Repo { get; init; }
public int PrNumber { get; init; }
public bool PostComment { get; init; }
- public string? Language { get; init; } = "en";
+ public string? Language { get; init; } = null;
///
/// Validates the common options shared across all commands
diff --git a/PRAgent/CommandLine/CommentCommandHandler.cs b/PRAgent/CommandLine/CommentCommandHandler.cs
new file mode 100644
index 0000000..b57c833
--- /dev/null
+++ b/PRAgent/CommandLine/CommentCommandHandler.cs
@@ -0,0 +1,125 @@
+using PRAgent.Models;
+using PRAgent.Services;
+using Serilog;
+
+namespace PRAgent.CommandLine;
+
+///
+/// Handles the comment command
+///
+public class CommentCommandHandler : ICommandHandler
+{
+ private readonly CommentCommandOptions _options;
+ private readonly IGitHubService _gitHubService;
+
+ public CommentCommandHandler(CommentCommandOptions options, IGitHubService gitHubService)
+ {
+ _options = options;
+ _gitHubService = gitHubService;
+ }
+
+ public async Task ExecuteAsync()
+ {
+ if (!_options.IsValid(out var errors))
+ {
+ Log.Error("Invalid options:");
+ foreach (var error in errors)
+ {
+ Log.Error(" - {Error}", error);
+ }
+ return 1;
+ }
+
+ try
+ {
+ // 最初にPR情報を取得してファイルを確認
+ var pr = await _gitHubService.GetPullRequestAsync(_options.Owner!, _options.Repo!, _options.PrNumber);
+
+ Console.WriteLine("以下のコメントを投稿しますか?");
+ Console.WriteLine($"PR: {pr.Title} (#{_options.PrNumber})");
+ Console.WriteLine();
+
+ foreach (var (comment, index) in _options.Comments.Select((c, i) => (c, i)))
+ {
+ if (comment == null) continue;
+
+ Console.WriteLine($"コメント {index + 1}:");
+ Console.WriteLine($" ファイル: {comment.FilePath}");
+ Console.WriteLine($" 行数: {comment.LineNumber}");
+ Console.WriteLine($" コメント: {comment.CommentText}");
+
+ if (!string.IsNullOrEmpty(comment.SuggestionText))
+ {
+ Console.WriteLine($" 修正案: {comment.SuggestionText}");
+ }
+ Console.WriteLine();
+ }
+
+ Console.Write("[投稿する] [キャンセル]: ");
+ var input = Console.ReadLine()?.Trim().ToLowerInvariant();
+
+ if (input == "投稿する" || input == "post" || input == "y" || input == "yes")
+ {
+ // 複数のコメントを一度に投稿
+ var commentList = _options.Comments
+ .Where(c => c != null)
+ .Select(c => (
+ FilePath: c!.FilePath,
+ LineNumber: (int?)c.LineNumber,
+ StartLine: (int?)null,
+ EndLine: (int?)null,
+ Comment: c.CommentText,
+ Suggestion: c.SuggestionText
+ ))
+ .ToList();
+
+ if (commentList.Any())
+ {
+ await _gitHubService.CreateMultipleLineCommentsAsync(
+ _options.Owner!,
+ _options.Repo!,
+ _options.PrNumber,
+ commentList
+ );
+
+ Console.WriteLine("コメントを投稿しました.");
+ }
+
+ // --approveオプションが指定されていた場合はPRを承認
+ if (_options.Approve)
+ {
+ Console.WriteLine("\nPRを承認しますか? [承認する] [キャンセル]: ");
+ var approveInput = Console.ReadLine()?.Trim().ToLowerInvariant();
+
+ if (approveInput == "承認する" || approveInput == "approve" || approveInput == "y" || approveInput == "yes")
+ {
+ await _gitHubService.ApprovePullRequestAsync(
+ _options.Owner!,
+ _options.Repo!,
+ _options.PrNumber,
+ "Approved by PRAgent with comments"
+ );
+ Console.WriteLine("PRを承認しました.");
+ }
+ else
+ {
+ Console.WriteLine("PRの承認をキャンセルしました.");
+ }
+ }
+
+ return 0;
+ }
+ else
+ {
+ Console.WriteLine("キャンセルしました。");
+ return 0;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Comment posting failed");
+ Console.WriteLine("コメントの投稿に失敗しました。");
+ return 1;
+ }
+ }
+}
diff --git a/PRAgent/Configuration/ServiceCollectionExtensions.cs b/PRAgent/Configuration/ServiceCollectionExtensions.cs
index e550e2d..cc41b56 100644
--- a/PRAgent/Configuration/ServiceCollectionExtensions.cs
+++ b/PRAgent/Configuration/ServiceCollectionExtensions.cs
@@ -1,8 +1,10 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using PRAgent.Agents;
+using PRAgent.Agents.SK;
using PRAgent.Models;
using PRAgent.Services;
+using PRAgent.Services.SK;
using PRAgent.Validators;
using Serilog;
@@ -65,15 +67,15 @@ public static IServiceCollection AddPRAgentServices(
// Data Services
services.AddSingleton();
- // Agents
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
+ // Detailed Comment Agent
services.AddSingleton();
- // Agent Orchestrator
- services.AddSingleton();
+ // SK Agents (Semantic Kernel Agent Framework)
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Agent Orchestrator - SKAgentOrchestratorServiceを使用
+ services.AddSingleton();
// PR Analysis Service
services.AddSingleton();
diff --git a/PRAgent/Configuration/YamlConfigurationProvider.cs b/PRAgent/Configuration/YamlConfigurationProvider.cs
deleted file mode 100644
index ca47b57..0000000
--- a/PRAgent/Configuration/YamlConfigurationProvider.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using YamlDotNet.Serialization;
-using YamlDotNet.Serialization.NamingConventions;
-
-namespace PRAgent.Configuration;
-
-public static class YamlConfigurationProvider
-{
- private static readonly IDeserializer Deserializer = new DeserializerBuilder()
- .WithNamingConvention(UnderscoredNamingConvention.Instance)
- .IgnoreUnmatchedProperties()
- .Build();
-
- public static T? Deserialize(string yaml) where T : class
- {
- if (string.IsNullOrWhiteSpace(yaml))
- {
- return default;
- }
-
- try
- {
- return Deserializer.Deserialize(yaml);
- }
- catch (Exception ex)
- {
- throw new InvalidOperationException($"Failed to parse YAML: {ex.Message}", ex);
- }
- }
-
- public static bool IsValidYaml(string yaml)
- {
- if (string.IsNullOrWhiteSpace(yaml))
- {
- return false;
- }
-
- try
- {
- Deserializer.Deserialize