From e1abb51574cfedb60eb7f6fd1fb45c2193825f81 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:11:15 +0900 Subject: [PATCH 01/27] [fix] use agent framework --- PRAgent.Tests/CommentCommandTests.cs | 320 ++++++++++++++++ PRAgent/Agents/AgentFactory.cs | 181 ++++++++++ PRAgent/Agents/ReviewAgent.cs | 84 +++++ PRAgent/Agents/SK/SKApprovalAgent.cs | 219 +++++++++++ PRAgent/Agents/SK/SKReviewAgent.cs | 103 ++++++ PRAgent/Agents/SK/SKSummaryAgent.cs | 103 ++++++ PRAgent/Agents/SelectionStrategies.cs | 262 ++++++++++++++ PRAgent/Agents/SummaryAgent.cs | 84 +++++ PRAgent/Models/CommentCommandOptions.cs | 273 ++++++++++++++ PRAgent/Models/PRActionBuffer.cs | 134 +++++++ PRAgent/Models/PRAgentYmlConfig.cs | 111 ++++-- PRAgent/PRAgent.csproj | 6 + .../Plugins/Agent/AgentInvocationFunctions.cs | 281 +++++++++++++++ PRAgent/Plugins/GitHub/ApprovePRFunction.cs | 111 ++++++ PRAgent/Plugins/GitHub/PostCommentFunction.cs | 250 +++++++++++++ PRAgent/Plugins/PRActionFunctions.cs | 130 +++++++ PRAgent/Program.cs | 341 +++++++++++++++++- PRAgent/Services/ConfigurationService.cs | 117 +++--- PRAgent/Services/GitHubService.cs | 40 ++ PRAgent/Services/IGitHubService.cs | 2 + PRAgent/Services/IKernelService.cs | 3 + PRAgent/Services/KernelService.cs | 46 +++ PRAgent/Services/PRActionExecutor.cs | 185 ++++++++++ .../Services/SK/SKAgentOrchestratorService.cs | 262 ++++++++++++++ PRAgent/appsettings.Development.json | 20 + PRAgent/appsettings.json | 35 ++ 26 files changed, 3602 insertions(+), 101 deletions(-) create mode 100644 PRAgent.Tests/CommentCommandTests.cs create mode 100644 PRAgent/Agents/AgentFactory.cs create mode 100644 PRAgent/Agents/SK/SKApprovalAgent.cs create mode 100644 PRAgent/Agents/SK/SKReviewAgent.cs create mode 100644 PRAgent/Agents/SK/SKSummaryAgent.cs create mode 100644 PRAgent/Agents/SelectionStrategies.cs create mode 100644 PRAgent/Models/CommentCommandOptions.cs create mode 100644 PRAgent/Models/PRActionBuffer.cs create mode 100644 PRAgent/Plugins/Agent/AgentInvocationFunctions.cs create mode 100644 PRAgent/Plugins/GitHub/ApprovePRFunction.cs create mode 100644 PRAgent/Plugins/GitHub/PostCommentFunction.cs create mode 100644 PRAgent/Plugins/PRActionFunctions.cs create mode 100644 PRAgent/Services/PRActionExecutor.cs create mode 100644 PRAgent/Services/SK/SKAgentOrchestratorService.cs create mode 100644 PRAgent/appsettings.Development.json 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/Agents/AgentFactory.cs b/PRAgent/Agents/AgentFactory.cs new file mode 100644 index 0000000..08a060a --- /dev/null +++ b/PRAgent/Agents/AgentFactory.cs @@ -0,0 +1,181 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +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); + } + + /// + /// Summaryエージェントを作成 + /// + public async Task CreateSummaryAgentAsync( + string owner, + string repo, + int prNumber, + string? customSystemPrompt = null, + IEnumerable? functions = null) + { + var kernel = _kernelService.CreateAgentKernel(AgentDefinition.SummaryAgent.SystemPrompt); + + if (functions != null) + { + foreach (var function in functions) + { + kernel.ImportPluginFromObject(function); + } + } + + var agent = new ChatCompletionAgent + { + Name = AgentDefinition.SummaryAgent.Name, + Description = AgentDefinition.SummaryAgent.Description, + Instructions = customSystemPrompt ?? AgentDefinition.SummaryAgent.SystemPrompt, + Kernel = kernel + }; + + return await Task.FromResult(agent); + } + + /// + /// Approvalエージェントを作成 + /// + public async Task CreateApprovalAgentAsync( + string owner, + string repo, + int prNumber, + string? customSystemPrompt = null, + IEnumerable? functions = null) + { + var kernel = _kernelService.CreateAgentKernel(AgentDefinition.ApprovalAgent.SystemPrompt); + + // GitHub操作用のプラグインを登録 + if (functions != null) + { + foreach (var function in functions) + { + kernel.ImportPluginFromObject(function); + } + } + + var agent = new ChatCompletionAgent + { + Name = AgentDefinition.ApprovalAgent.Name, + Description = AgentDefinition.ApprovalAgent.Description, + Instructions = customSystemPrompt ?? AgentDefinition.ApprovalAgent.SystemPrompt, + Kernel = kernel, + Arguments = new KernelArguments + { + // Approvalエージェント用の特殊設定 + ["approval_mode"] = true, + ["owner"] = owner, + ["repo"] = repo, + ["pr_number"] = prNumber + } + }; + + return await Task.FromResult(agent); + } + + /// + /// カスタムエージェントを作成(汎用メソッド) + /// + 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); + } + + /// + /// 複数のエージェントを一度に作成 + /// + public async Task<(ChatCompletionAgent reviewAgent, ChatCompletionAgent summaryAgent, ChatCompletionAgent approvalAgent)> CreateAllAgentsAsync( + string owner, + string repo, + int prNumber, + string? customReviewPrompt = null, + string? customSummaryPrompt = null, + string? customApprovalPrompt = null) + { + var reviewAgent = await CreateReviewAgentAsync(owner, repo, prNumber, customReviewPrompt); + var summaryAgent = await CreateSummaryAgentAsync(owner, repo, prNumber, customSummaryPrompt); + var approvalAgent = await CreateApprovalAgentAsync(owner, repo, prNumber, customApprovalPrompt); + + return (reviewAgent, summaryAgent, approvalAgent); + } +} diff --git a/PRAgent/Agents/ReviewAgent.cs b/PRAgent/Agents/ReviewAgent.cs index 0ebcd23..3848bd8 100644 --- a/PRAgent/Agents/ReviewAgent.cs +++ b/PRAgent/Agents/ReviewAgent.cs @@ -1,5 +1,8 @@ using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Models; using PRAgent.Services; +using PRAgent.Plugins; namespace PRAgent.Agents; @@ -32,4 +35,85 @@ You are an expert code reviewer with deep knowledge of software engineering best return await KernelService.InvokePromptAsStringAsync(CreateKernel(), prompt, cancellationToken); } + + /// + /// Function Callingを使用してレビューを実行し、アクションをバッファに蓄積します + /// + public async Task ReviewWithActionsAsync( + string owner, + string repo, + int prNumber, + PRActionBuffer buffer, + CancellationToken cancellationToken = default) + { + var (pr, files, diff) = await GetPRDataAsync(owner, repo, prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // Kernelを作成して関数を登録 + var kernel = CreateKernel(); + var actionFunctions = new PRActionFunctions(buffer); + kernel.ImportPluginFromObject(actionFunctions, "pr_actions"); + + 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. + + You have access to the following functions to accumulate your review actions: + - add_line_comment: Add a comment on a specific line of code + - add_review_comment: Add a general review comment + - add_summary: Add a summary of your review + + Instructions: + 1. Review the code changes thoroughly + 2. For each issue you find, use add_line_comment to mark the specific location + 3. If you have suggestions, include them in the line comment + 4. Use add_review_comment for general observations that don't apply to specific lines + 5. Use add_summary to provide an overall summary of your review + 6. When done, use ready_to_commit to indicate you're finished + + Only mark CRITICAL and MAJOR issues. Ignore MINOR issues unless there are many of them. + + File format for add_line_comment: use the exact file path as shown in the diff + """; + + var prompt = $""" + {systemPrompt} + + ## Pull Request Information + - Title: {pr.Title} + - Author: {pr.User.Login} + - Description: {pr.Body ?? "No description provided"} + - Branch: {pr.Head.Ref} -> {pr.Base.Ref} + + ## Changed Files + {fileList} + + ## Diff + {diff} + + Please review this pull request and use the available functions to add comments and mark issues. + When you're done, call ready_to_commit. + """; + + // 注: Semantic Kernel 1.68.0でのFunction Callingは、複雑なTool Call処理が必要です + // 現在は簡易的な実装として、通常のレビューを実行します + // 将来的には、Auto Commitオプションで完全なFunction Callingを実装予定 + + var resultBuilder = new System.Text.StringBuilder(); + + // 通常のレビューを実行して結果を取得 + var reviewResult = await KernelService.InvokePromptAsStringAsync(kernel, prompt, cancellationToken); + resultBuilder.Append(reviewResult); + + // 注: 実際のFunction Calling実装時は、ここでTool Callを処理してバッファに追加 + + // バッファの状態を追加(デモ用) + var state = buffer.GetState(); + resultBuilder.AppendLine($"\n\n## Review Summary"); + resultBuilder.AppendLine($"Line comments added: {state.LineCommentCount}"); + resultBuilder.AppendLine($"Review comments added: {state.ReviewCommentCount}"); + resultBuilder.AppendLine($"Summaries added: {state.SummaryCount}"); + + return resultBuilder.ToString(); + } } diff --git a/PRAgent/Agents/SK/SKApprovalAgent.cs b/PRAgent/Agents/SK/SKApprovalAgent.cs new file mode 100644 index 0000000..2ec2f3c --- /dev/null +++ b/PRAgent/Agents/SK/SKApprovalAgent.cs @@ -0,0 +1,219 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Models; +using PRAgent.Services; +using PRAgent.Plugins.GitHub; +using PRAgentDefinition = PRAgent.Agents.AgentDefinition; + +namespace PRAgent.Agents.SK; + +/// +/// Semantic Kernel ChatCompletionAgentベースの承認エージェント +/// +public class SKApprovalAgent +{ + private readonly PRAgentFactory _agentFactory; + private readonly IGitHubService _gitHubService; + private readonly PullRequestDataService _prDataService; + + public SKApprovalAgent( + PRAgentFactory agentFactory, + IGitHubService gitHubService, + PullRequestDataService prDataService) + { + _agentFactory = agentFactory; + _gitHubService = gitHubService; + _prDataService = prDataService; + } + + /// + /// レビュー結果に基づいて承認決定を行います + /// + public async Task<(bool ShouldApprove, string Reasoning, string? Comment)> DecideAsync( + string owner, + string repo, + int prNumber, + string reviewResult, + ApprovalThreshold threshold, + CancellationToken cancellationToken = default) + { + // PR情報を取得 + 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 agent = await _agentFactory.CreateApprovalAgentAsync(owner, repo, prNumber); + + 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 ApprovalResponseParser.Parse(responses.ToString()); + } + + /// + /// プルリクエストを承認します + /// + 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}"; + } + + /// + /// レビューと承認を自動的に行います(承認条件を満たす場合) + /// + public async Task<(bool Approved, string Reasoning)> ReviewAndApproveAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + CancellationToken cancellationToken = default) + { + // まずReviewエージェントを呼び出してレビューを実行 + var reviewAgent = new SKReviewAgent(_agentFactory, _prDataService); + var reviewResult = await reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + + // 決定を行う + var (shouldApprove, reasoning, comment) = await DecideAsync( + owner, repo, prNumber, reviewResult, threshold, cancellationToken); + + // 承認する場合、実際に承認を実行 + if (shouldApprove) + { + await ApproveAsync(owner, repo, prNumber, comment); + return (true, $"Approved: {reasoning}"); + } + + return (false, $"Not approved: {reasoning}"); + } + + /// + /// GitHub操作関数を持つApprovalエージェントを作成します + /// + public async Task CreateAgentWithGitHubFunctionsAsync( + string owner, + string repo, + int prNumber, + string? customSystemPrompt = null) + { + var kernel = _agentFactory; + + // GitHub操作用のプラグインを作成 + var approveFunction = new ApprovePRFunction(_gitHubService, owner, repo, prNumber); + var commentFunction = new PostCommentFunction(_gitHubService, owner, repo, prNumber); + + // 関数をKernelFunctionとして登録 + var functions = new List + { + ApprovePRFunction.ApproveAsyncFunction(_gitHubService, owner, repo, prNumber), + ApprovePRFunction.GetApprovalStatusFunction(_gitHubService, owner, repo, prNumber), + PostCommentFunction.PostCommentAsyncFunction(_gitHubService, owner, repo, prNumber), + PostCommentFunction.PostReviewCommentAsyncFunction(_gitHubService, owner, repo, prNumber), + PostCommentFunction.PostLineCommentAsyncFunction(_gitHubService, owner, repo, prNumber) + }; + + return await _agentFactory.CreateApprovalAgentAsync( + owner, repo, prNumber, customSystemPrompt, functions); + } + + /// + /// 関数呼び出し機能を持つApprovalエージェントを使用して決定を行います + /// + public async Task<(bool ShouldApprove, string Reasoning, string? Comment)> DecideWithFunctionCallingAsync( + string owner, + string repo, + int prNumber, + string reviewResult, + ApprovalThreshold threshold, + bool autoApprove = false, + CancellationToken cancellationToken = default) + { + // GitHub関数を持つエージェントを作成 + var agent = await CreateAgentWithGitHubFunctionsAsync(owner, repo, prNumber); + + // PR情報を取得 + var pr = await _gitHubService.GetPullRequestAsync(owner, repo, prNumber); + var thresholdDescription = ApprovalThresholdHelper.GetDescription(threshold); + + // プロンプトを作成 + var autoApproveInstruction = autoApprove + ? "If the decision is to APPROVE, use the approve_pull_request function to actually approve the PR." + : ""; + + 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} + + Your task: + 1. Analyze the review results against the approval threshold + 2. Make a decision (APPROVE or REJECT) + 3. {autoApproveInstruction} + 4. If there are specific concerns that need to be addressed, use post_pr_comment or post_line_comment + + 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 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 ApprovalResponseParser.Parse(responses.ToString()); + } +} diff --git a/PRAgent/Agents/SK/SKReviewAgent.cs b/PRAgent/Agents/SK/SKReviewAgent.cs new file mode 100644 index 0000000..811481f --- /dev/null +++ b/PRAgent/Agents/SK/SKReviewAgent.cs @@ -0,0 +1,103 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Services; +using PRAgentDefinition = PRAgent.Agents.AgentDefinition; + +namespace PRAgent.Agents.SK; + +/// +/// Semantic Kernel ChatCompletionAgentベースのレビューエージェント +/// +public class SKReviewAgent +{ + private readonly PRAgentFactory _agentFactory; + private readonly PullRequestDataService _prDataService; + + public SKReviewAgent( + PRAgentFactory agentFactory, + PullRequestDataService prDataService) + { + _agentFactory = agentFactory; + _prDataService = prDataService; + } + + /// + /// プルリクエストのコードレビューを実行します + /// + 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; + } + } + + /// + /// 指定された関数(プラグイン)を持つReviewエージェントを作成します + /// + public async Task CreateAgentWithFunctionsAsync( + string owner, + string repo, + int prNumber, + IEnumerable functions, + string? customSystemPrompt = null) + { + return await _agentFactory.CreateReviewAgentAsync( + owner, repo, prNumber, customSystemPrompt, functions); + } +} diff --git a/PRAgent/Agents/SK/SKSummaryAgent.cs b/PRAgent/Agents/SK/SKSummaryAgent.cs new file mode 100644 index 0000000..e8b4b55 --- /dev/null +++ b/PRAgent/Agents/SK/SKSummaryAgent.cs @@ -0,0 +1,103 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Services; +using PRAgentDefinition = PRAgent.Agents.AgentDefinition; + +namespace PRAgent.Agents.SK; + +/// +/// Semantic Kernel ChatCompletionAgentベースのサマリーエージェント +/// +public class SKSummaryAgent +{ + private readonly PRAgentFactory _agentFactory; + private readonly PullRequestDataService _prDataService; + + public SKSummaryAgent( + PRAgentFactory agentFactory, + PullRequestDataService prDataService) + { + _agentFactory = agentFactory; + _prDataService = prDataService; + } + + /// + /// プルリクエストの要約を作成します + /// + public async Task SummarizeAsync( + string owner, + string repo, + int prNumber, + string? customSystemPrompt = null, + CancellationToken cancellationToken = default) + { + // Summaryエージェントを作成 + var agent = await _agentFactory.CreateSummaryAgentAsync(owner, repo, prNumber, customSystemPrompt); + + // PRデータを取得 + var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // プロンプトを作成 + var systemPrompt = customSystemPrompt ?? PRAgentDefinition.SummaryAgent.SystemPrompt; + var prompt = PullRequestDataService.CreateSummaryPrompt(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 SummarizeStreamingAsync( + string owner, + string repo, + int prNumber, + string? customSystemPrompt = null, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Summaryエージェントを作成 + var agent = await _agentFactory.CreateSummaryAgentAsync(owner, repo, prNumber, customSystemPrompt); + + // PRデータを取得 + var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // プロンプトを作成 + var systemPrompt = customSystemPrompt ?? PRAgentDefinition.SummaryAgent.SystemPrompt; + var prompt = PullRequestDataService.CreateSummaryPrompt(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; + } + } + + /// + /// 指定された関数(プラグイン)を持つSummaryエージェントを作成します + /// + public async Task CreateAgentWithFunctionsAsync( + string owner, + string repo, + int prNumber, + IEnumerable functions, + string? customSystemPrompt = null) + { + return await _agentFactory.CreateSummaryAgentAsync( + owner, repo, prNumber, customSystemPrompt, functions); + } +} 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 index dde6f78..a40e24f 100644 --- a/PRAgent/Agents/SummaryAgent.cs +++ b/PRAgent/Agents/SummaryAgent.cs @@ -1,5 +1,8 @@ using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Models; using PRAgent.Services; +using PRAgent.Plugins; namespace PRAgent.Agents; @@ -32,4 +35,85 @@ Your role is to summarize pull request changes accurately. return await KernelService.InvokePromptAsStringAsync(CreateKernel(), prompt, cancellationToken); } + + /// + /// Function Callingを使用してサマリーを作成し、アクションをバッファに蓄積します + /// + public async Task SummarizeWithActionsAsync( + string owner, + string repo, + int prNumber, + PRActionBuffer buffer, + CancellationToken cancellationToken = default) + { + var (pr, files, diff) = await GetPRDataAsync(owner, repo, prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // Kernelを作成して関数を登録 + var kernel = CreateKernel(); + var actionFunctions = new PRActionFunctions(buffer); + kernel.ImportPluginFromObject(actionFunctions, "pr_actions"); + + var systemPrompt = """ + You are a technical writer specializing in creating clear, concise documentation. + Your role is to summarize pull request changes accurately. + + You have access to the following functions: + - add_summary: Add a summary of the pull request + - set_general_comment: Set a general comment to post + - ready_to_commit: Indicate you're finished + + Instructions: + 1. Analyze the pull request changes + 2. Create a concise summary (under 300 words) using add_summary + 3. Optionally add additional context using set_general_comment + 4. When done, call ready_to_commit + + Focus on: + - Purpose: What does this PR achieve? + - Key Changes: Main files/components modified + - Impact: Areas affected + - Risk Assessment: Low/Medium/High with justification + - Testing Notes: Areas needing special attention + """; + + var prompt = $""" + {systemPrompt} + + ## Pull Request + - Title: {pr.Title} + - Author: {pr.User.Login} + - Description: {pr.Body ?? "No description provided"} + - Branch: {pr.Head.Ref} -> {pr.Base.Ref} + + ## Changed Files + {fileList} + + ## Diff + {diff} + + Please summarize this pull request and use the available functions to add the summary. + When you're done, call ready_to_commit. + """; + + // 注: Semantic Kernel 1.68.0でのFunction Callingは、複雑なTool Call処理が必要です + // 現在は簡易的な実装として、通常のサマリーを実行します + // 将来的には、Auto Commitオプションで完全なFunction Callingを実装予定 + + var resultBuilder = new System.Text.StringBuilder(); + + // 通常のサマリーを実行して結果を取得 + var summaryResult = await KernelService.InvokePromptAsStringAsync(kernel, prompt, cancellationToken); + resultBuilder.Append(summaryResult); + + // 注: 実際のFunction Calling実装時は、ここでTool Callを処理してバッファに追加 + + // バッファの状態を追加(デモ用) + var state = buffer.GetState(); + resultBuilder.AppendLine($"\n\n## Summary Summary"); + resultBuilder.AppendLine($"Summaries added: {state.SummaryCount}"); + resultBuilder.AppendLine($"General comment set: {state.HasGeneralComment}"); + + return resultBuilder.ToString(); + } } diff --git a/PRAgent/Models/CommentCommandOptions.cs b/PRAgent/Models/CommentCommandOptions.cs new file mode 100644 index 0000000..2ec1868 --- /dev/null +++ b/PRAgent/Models/CommentCommandOptions.cs @@ -0,0 +1,273 @@ +using System.Text.RegularExpressions; + +namespace PRAgent.Models; + +public record CommentCommandOptions +{ + public string? Owner { get; init; } + public string? Repo { get; init; } + public int PrNumber { get; init; } + public List Comments { get; init; } = new(); + public bool Approve { get; init; } + public bool IsValid(out List errors) + { + errors = new List(); + + if (string.IsNullOrEmpty(Owner)) + errors.Add("--owner is required"); + if (string.IsNullOrEmpty(Repo)) + errors.Add("--repo is required"); + if (PrNumber <= 0) + errors.Add("--pr is required and must be a positive number"); + + // 有効なコメントが1つ以上あるかチェック + if (!Comments.Any()) + errors.Add("No valid comments specified"); + + foreach (var (comment, index) in Comments.Select((c, i) => (c, i))) + { + if (comment == null) + { + errors.Add($"Comment {index + 1}: Invalid comment format"); + continue; + } + + // パース段階でチェック済みのため、ここでは不要 + // if (string.IsNullOrWhiteSpace(comment.CommentText)) + // { + // if (string.IsNullOrEmpty(comment.CommentText)) + // { + // errors.Add($"Comment {index + 1}: Comment text is required"); + // } + // else + // { + // errors.Add($"Comment {index + 1}: Comment text cannot be whitespace only"); + // } + // } + + if (comment.LineNumber <= 0) + { + errors.Add($"Comment {index + 1}: Line number must be greater than 0"); + } + + if (!string.IsNullOrEmpty(comment.FilePath) && !IsValidFilePath(comment.FilePath)) + { + errors.Add($"Comment {index + 1}: Invalid file path format"); + } + } + + return errors.Count == 0; + } + + private static bool IsValidFilePath(string path) + { + try + { + // 基本的なファイルパスの検証 + return !string.IsNullOrWhiteSpace(path) && + !path.StartsWith("..") && + !path.Contains("/") && + !path.Contains("\\"); + } + catch + { + return false; + } + } + + public static CommentCommandOptions Parse(string[] args) + { + var options = new CommentCommandOptions(); + var commentArgs = new List(); + + // commentコマンド以降の引数を収集 + bool inCommentSection = false; + foreach (var arg in args) + { + if (arg == "comment") + { + inCommentSection = true; + continue; + } + + if (inCommentSection) + { + commentArgs.Add(arg); + } + } + + options = ParseCommentOptions(args, options); + options = options with { Comments = ParseComments(commentArgs.ToArray()) }; + + return options; + } + + private static CommentCommandOptions ParseCommentOptions(string[] args, CommentCommandOptions current) + { + var options = current; + + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--owner": + case "-o": + if (i + 1 < args.Length) + options = options with { Owner = args[++i] }; + break; + case "--repo": + case "-r": + if (i + 1 < args.Length) + options = options with { Repo = args[++i] }; + break; + case "--pr": + case "-p": + case "--number": + if (i + 1 < args.Length && int.TryParse(args[++i], out var pr)) + options = options with { PrNumber = pr }; + break; + case "--approve": + options = options with { Approve = true }; + break; + } + } + + return options; + } + + private static List ParseComments(string[] commentArgs) + { + var comments = new List(); + + // オプション引数(--owner, --repo, --pr, --approve)とその値をスキップしながらパース + // ただし、--suggestionはコメント処理内で扱う + + var skipNext = false; + + for (int i = 0; i < commentArgs.Length; i++) + { + // 前の引数がオプションの値だった場合はスキップ + if (skipNext) + { + skipNext = false; + continue; + } + + var arg = commentArgs[i]; + + // --owner, --repo, --pr, --approve などのオプションはスキップ + if (arg == "--owner" || arg == "-o" || arg == "--repo" || arg == "-r" || + arg == "--pr" || arg == "-p" || arg == "--number" || arg == "--approve") + { + skipNext = true; + continue; + } + + // --suggestion は最後のコメントにsuggestionを追加 + if (arg == "--suggestion" || arg.Equals("--suggestion", StringComparison.OrdinalIgnoreCase)) + { + if (i + 1 < commentArgs.Length && comments.Count > 0) + { + var suggestionText = commentArgs[i + 1]; + if (!string.IsNullOrWhiteSpace(suggestionText)) + { + var lastComment = comments[^1]; + comments[^1] = lastComment with { SuggestionText = suggestionText }; + } + i++; // --suggestionの値もスキップ + } + continue; + } + + // @で始まる、または@を含む引数はコメントとして処理 + if (arg.StartsWith("@") || arg.Contains("@")) + { + string lineRange; + + if (arg.StartsWith("@")) + { + // @123 形式 + lineRange = arg.Substring(1); + } + else + { + // src/file.cs@123 形式 - 全体を渡す + lineRange = arg; + } + + if (TryParseLineRange(lineRange, out var lineNumber, out var filePath) && lineNumber > 0) + { + // 次の引数をコメントテキストとして取得 + if (i + 1 < commentArgs.Length) + { + var textArg = commentArgs[i + 1]; + + // 次の引数がオプションでない場合はコメントテキストとして扱う + if (!textArg.StartsWith("-") && !string.IsNullOrWhiteSpace(textArg)) + { + comments.Add(new CommentTarget( + LineNumber: lineNumber, + FilePath: filePath ?? "src/index.cs", + CommentText: textArg, + SuggestionText: null // 後で --suggestion で設定される + )); + i++; // コメントテキストをスキップ + } + } + } + } + } + + return comments; + } + + private static bool TryParseLineRange(string lineRange, out int lineNumber, out string? filePath) + { + lineNumber = 0; + filePath = null; + + if (string.IsNullOrWhiteSpace(lineRange)) + return false; + + // ファイルパスを含む形式(例: src/file.cs@123)を最初にチェック + if (lineRange.Contains("@")) + { + var parts = lineRange.Split('@', 2); + if (parts.Length == 2 && + int.TryParse(parts[1], out var fileLine) && + fileLine > 0 && // 行数は正の数のみ + !string.IsNullOrWhiteSpace(parts[0])) + { + lineNumber = fileLine; + filePath = parts[0]; + return true; + } + } + + // 行範囲指定形式(例: 123, 45-67) + if (lineRange.Contains("-")) + { + var parts = lineRange.Split('-', 2); + if (parts.Length == 2 && int.TryParse(parts[0], out var startLine) && startLine > 0) + { + lineNumber = startLine; + return true; + } + } + // 単一行指定形式(例: 123) + else if (int.TryParse(lineRange, out var singleLine) && singleLine > 0) + { + lineNumber = singleLine; + return true; + } + + return false; + } +} + +public record CommentTarget( + int LineNumber, + string FilePath, + string CommentText, + string? SuggestionText +); \ No newline at end of file diff --git a/PRAgent/Models/PRActionBuffer.cs b/PRAgent/Models/PRActionBuffer.cs new file mode 100644 index 0000000..7002ae7 --- /dev/null +++ b/PRAgent/Models/PRActionBuffer.cs @@ -0,0 +1,134 @@ +namespace PRAgent.Models; + +/// +/// PRアクションを蓄積するバッファクラス +/// エージェントが実行中にコメントやサマリーを追加し、最後にまとめて投稿できる +/// +public class PRActionBuffer +{ + private readonly List _lineComments = new(); + private readonly List _reviewComments = new(); + private readonly List _summaries = new(); + private string? _generalComment; + private bool _shouldApprove; + private string? _approvalComment; + + /// + /// 行コメントを追加します + /// + public void AddLineComment(string filePath, int lineNumber, string comment, string? suggestion = null) + { + _lineComments.Add(new LineCommentAction + { + FilePath = filePath, + LineNumber = lineNumber, + Comment = comment, + Suggestion = suggestion + }); + } + + /// + /// レビューコメントを追加します + /// + public void AddReviewComment(string comment) + { + _reviewComments.Add(new ReviewCommentAction + { + Comment = comment + }); + } + + /// + /// サマリーを追加します + /// + public void AddSummary(string summary) + { + _summaries.Add(summary); + } + + /// + /// 全体コメントを設定します + /// + public void SetGeneralComment(string comment) + { + _generalComment = comment; + } + + /// + /// 承認をマークします + /// + public void MarkForApproval(string? comment = null) + { + _shouldApprove = true; + _approvalComment = comment; + } + + /// + /// バッファをクリアします + /// + public void Clear() + { + _lineComments.Clear(); + _reviewComments.Clear(); + _summaries.Clear(); + _generalComment = null; + _shouldApprove = false; + _approvalComment = null; + } + + /// + /// バッファの状態を取得します + /// + public PRActionState GetState() + { + return new PRActionState + { + LineCommentCount = _lineComments.Count, + ReviewCommentCount = _reviewComments.Count, + SummaryCount = _summaries.Count, + HasGeneralComment = !string.IsNullOrEmpty(_generalComment), + ShouldApprove = _shouldApprove + }; + } + + /// + /// 蓄積されたアクションを実行するためのデータを取得します + /// + public IReadOnlyList LineComments => _lineComments.AsReadOnly(); + public IReadOnlyList ReviewComments => _reviewComments.AsReadOnly(); + public IReadOnlyList Summaries => _summaries.AsReadOnly(); + public string? GeneralComment => _generalComment; + public bool ShouldApprove => _shouldApprove; + public string? ApprovalComment => _approvalComment; +} + +/// +/// 行コメントアクション +/// +public class LineCommentAction +{ + public required string FilePath { get; init; } + public required int LineNumber { get; init; } + public required string Comment { get; init; } + public string? Suggestion { get; init; } +} + +/// +/// レビューコメントアクション +/// +public class ReviewCommentAction +{ + public required string Comment { get; init; } +} + +/// +/// PRアクションの状態 +/// +public class PRActionState +{ + public int LineCommentCount { get; init; } + public int ReviewCommentCount { get; init; } + public int SummaryCount { get; init; } + public bool HasGeneralComment { get; init; } + public bool ShouldApprove { get; init; } +} diff --git a/PRAgent/Models/PRAgentYmlConfig.cs b/PRAgent/Models/PRAgentYmlConfig.cs index 86b7c19..7b6b619 100644 --- a/PRAgent/Models/PRAgentYmlConfig.cs +++ b/PRAgent/Models/PRAgentYmlConfig.cs @@ -1,63 +1,39 @@ -using YamlDotNet.Serialization; - namespace PRAgent.Models; public class PRAgentYmlConfig { - [YamlMember(Alias = "pragent")] public PRAgentConfig? PRAgent { get; set; } } public class PRAgentConfig { public bool Enabled { get; set; } = true; - - [YamlMember(Alias = "system_prompt")] public string? SystemPrompt { get; set; } - - [YamlMember(Alias = "review")] public ReviewConfig? Review { get; set; } - - [YamlMember(Alias = "summary")] public SummaryConfig? Summary { get; set; } - - [YamlMember(Alias = "approve")] public ApproveConfig? Approve { get; set; } - - [YamlMember(Alias = "ignore_paths")] public List? IgnorePaths { get; set; } + public AgentFrameworkConfig? AgentFramework { get; set; } } public class ReviewConfig { public bool Enabled { get; set; } = true; - - [YamlMember(Alias = "auto_post")] public bool AutoPost { get; set; } = false; - - [YamlMember(Alias = "custom_prompt")] public string? CustomPrompt { get; set; } } public class SummaryConfig { public bool Enabled { get; set; } = true; - - [YamlMember(Alias = "post_as_comment")] public bool PostAsComment { get; set; } = true; - - [YamlMember(Alias = "custom_prompt")] public string? CustomPrompt { get; set; } } public class ApproveConfig { public bool Enabled { get; set; } = true; - - [YamlMember(Alias = "auto_approve_threshold")] public string AutoApproveThreshold { get; set; } = "minor"; - - [YamlMember(Alias = "require_review_first")] public bool RequireReviewFirst { get; set; } = true; } @@ -68,3 +44,88 @@ public enum ApprovalThreshold Minor, None } + +/// +/// Semantic Kernel Agent Frameworkの設定 +/// +public class AgentFrameworkConfig +{ + /// + /// Agent Frameworkを有効にするかどうか + /// + public bool Enabled { get; set; } = false; + + /// + /// 使用するオーケストレーションモード + /// + public string OrchestrationMode { get; set; } = "sequential"; // sequential, agent_chat, collaborative, parallel + + /// + /// 選択戦略 + /// + public string SelectionStrategy { get; set; } = "approval_workflow"; // sequential, approval_workflow, conditional, round_robin, content_based + + /// + /// 関数呼び出しを有効にするかどうか + /// + public bool EnableFunctionCalling { get; set; } = true; + + /// + /// 自動承認を有効にするかどうか + /// + public bool EnableAutoApproval { get; set; } = false; + + /// + /// 最大ターン数(AgentGroupChat用) + /// + public int MaxTurns { get; set; } = 10; + + /// + /// 各エージェントの設定 + /// + public AgentConfigs? Agents { get; set; } +} + +/// +/// 個別エージェントの設定 +/// +public class AgentConfigs +{ + public ReviewAgentConfig? Review { get; set; } + public SummaryAgentConfig? Summary { get; set; } + public ApprovalAgentConfig? Approval { get; set; } +} + +/// +/// Reviewエージェントの設定 +/// +public class ReviewAgentConfig +{ + public string? CustomSystemPrompt { get; set; } + public bool EnablePlugins { get; set; } = false; + public int? MaxTokens { get; set; } + public double? Temperature { get; set; } +} + +/// +/// Summaryエージェントの設定 +/// +public class SummaryAgentConfig +{ + public string? CustomSystemPrompt { get; set; } + public bool EnablePlugins { get; set; } = false; + public int? MaxTokens { get; set; } + public double? Temperature { get; set; } +} + +/// +/// Approvalエージェントの設定 +/// +public class ApprovalAgentConfig +{ + public string? CustomSystemPrompt { get; set; } + public bool EnableGitHubFunctions { get; set; } = true; + public bool AutoApproveOnDecision { get; set; } = false; + public int? MaxTokens { get; set; } + public double? Temperature { get; set; } +} diff --git a/PRAgent/PRAgent.csproj b/PRAgent/PRAgent.csproj index 050a99f..ef0cf74 100644 --- a/PRAgent/PRAgent.csproj +++ b/PRAgent/PRAgent.csproj @@ -6,10 +6,14 @@ enable enable Linux + + $(NoWarn);SKEXP0110;SKEXP0111;SKEXP0112;SKEXP0113;SKEXP0114;SKEXP0115 + + @@ -25,4 +29,6 @@ + + diff --git a/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs b/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs new file mode 100644 index 0000000..de80acf --- /dev/null +++ b/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs @@ -0,0 +1,281 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Agents; +using PRAgent.Services; +using PRAgentDefinition = PRAgent.Agents.AgentDefinition; + +namespace PRAgent.Plugins.Agent; + +/// +/// Agent-as-Functionパターンを実装するプラグイン +/// 他のエージェントを関数として呼び出すことを可能にします +/// +public class AgentInvocationFunctions +{ + private readonly PRAgentFactory _agentFactory; + private readonly PullRequestDataService _prDataService; + private readonly string _owner; + private readonly string _repo; + private readonly int _prNumber; + + public AgentInvocationFunctions( + PRAgentFactory agentFactory, + PullRequestDataService prDataService, + string owner, + string repo, + int prNumber) + { + _agentFactory = agentFactory; + _prDataService = prDataService; + _owner = owner; + _repo = repo; + _prNumber = prNumber; + } + + /// + /// Reviewエージェントを呼び出してコードレビューを実行します + /// + /// カスタムプロンプト(オプション) + /// キャンセレーショントークン + /// レビュー結果 + [KernelFunction("invoke_review_agent")] + public async Task InvokeReviewAgentAsync( + string? customPrompt = null, + CancellationToken cancellationToken = default) + { + try + { + // Reviewエージェントを作成 + var agent = await _agentFactory.CreateReviewAgentAsync(_owner, _repo, _prNumber); + + // PRデータを取得 + var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(_owner, _repo, _prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // プロンプトを作成 + var prompt = string.IsNullOrEmpty(customPrompt) + ? PullRequestDataService.CreateReviewPrompt(pr, fileList, diff, PRAgentDefinition.ReviewAgent.SystemPrompt) + : customPrompt; + + // チャット履歴を作成してエージェントを実行 + 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(); + } + catch (Exception ex) + { + return $"Error invoking review agent: {ex.Message}"; + } + } + + /// + /// Summaryエージェントを呼び出して要約を作成します + /// + /// カスタムプロンプト(オプション) + /// キャンセレーショントークン + /// 要約結果 + [KernelFunction("invoke_summary_agent")] + public async Task InvokeSummaryAgentAsync( + string? customPrompt = null, + CancellationToken cancellationToken = default) + { + try + { + // Summaryエージェントを作成 + var agent = await _agentFactory.CreateSummaryAgentAsync(_owner, _repo, _prNumber); + + // PRデータを取得 + var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(_owner, _repo, _prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // プロンプトを作成 + var prompt = string.IsNullOrEmpty(customPrompt) + ? PullRequestDataService.CreateSummaryPrompt(pr, fileList, diff, PRAgentDefinition.SummaryAgent.SystemPrompt) + : customPrompt; + + // チャット履歴を作成してエージェントを実行 + 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(); + } + catch (Exception ex) + { + return $"Error invoking summary agent: {ex.Message}"; + } + } + + /// + /// Reviewエージェントを関数として呼び出すためのKernelFunctionを作成します + /// + public static KernelFunction InvokeReviewAgentFunction( + PRAgentFactory agentFactory, + PullRequestDataService prDataService, + string owner, + string repo, + int prNumber) + { + var invocationPlugin = new AgentInvocationFunctions(agentFactory, prDataService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string? customPrompt, CancellationToken ct) => invocationPlugin.InvokeReviewAgentAsync(customPrompt, ct), + functionName: "invoke_review_agent", + description: "Invokes the review agent to perform code review on a pull request", + parameters: new[] + { + new KernelParameterMetadata("customPrompt") + { + Description = "Optional custom prompt for the review agent", + IsRequired = false, + DefaultValue = null + } + }); + } + + /// + /// Summaryエージェントを関数として呼び出すためのKernelFunctionを作成します + /// + public static KernelFunction InvokeSummaryAgentFunction( + PRAgentFactory agentFactory, + PullRequestDataService prDataService, + string owner, + string repo, + int prNumber) + { + var invocationPlugin = new AgentInvocationFunctions(agentFactory, prDataService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string? customPrompt, CancellationToken ct) => invocationPlugin.InvokeSummaryAgentAsync(customPrompt, ct), + functionName: "invoke_summary_agent", + description: "Invokes the summary agent to create a summary of a pull request", + parameters: new[] + { + new KernelParameterMetadata("customPrompt") + { + Description = "Optional custom prompt for the summary agent", + IsRequired = false, + DefaultValue = null + } + }); + } + + /// + /// Approvalエージェントを呼び出して承認決定を行います + /// + /// レビュー結果 + /// 承認閾値 + /// キャンセレーショントークン + /// 承認決定結果 + [KernelFunction("invoke_approval_agent")] + public async Task InvokeApprovalAgentAsync( + string reviewResult, + string threshold = "major", + CancellationToken cancellationToken = default) + { + try + { + // Approvalエージェントを作成 + var agent = await _agentFactory.CreateApprovalAgentAsync(_owner, _repo, _prNumber); + + // PRデータを取得 + var pr = await _prDataService.GetPullRequestDataAsync(_owner, _repo, _prNumber); + + // プロンプトを作成 + var thresholdDescription = GetThresholdDescription(threshold); + var prompt = $""" + Based on the code review below, make an approval decision for this pull request. + + ## Pull Request + - Title: {pr.Item1.Title} + - Author: {pr.Item1.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 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(); + } + catch (Exception ex) + { + return $"Error invoking approval agent: {ex.Message}"; + } + } + + private static string GetThresholdDescription(string threshold) + { + return threshold.ToLower() switch + { + "critical" => "critical: PR must have NO critical issues", + "major" => "major: PR must have NO major or critical issues", + "minor" => "minor: PR must have NO minor, major, or critical issues", + "none" => "none: Always approve", + _ => "major: PR must have NO major or critical issues (default)" + }; + } + + /// + /// Approvalエージェントを関数として呼び出すためのKernelFunctionを作成します + /// + public static KernelFunction InvokeApprovalAgentFunction( + PRAgentFactory agentFactory, + PullRequestDataService prDataService, + string owner, + string repo, + int prNumber) + { + var invocationPlugin = new AgentInvocationFunctions(agentFactory, prDataService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string reviewResult, string threshold, CancellationToken ct) => + invocationPlugin.InvokeApprovalAgentAsync(reviewResult, threshold, ct), + functionName: "invoke_approval_agent", + description: "Invokes the approval agent to make an approval decision based on review results", + parameters: new[] + { + new KernelParameterMetadata("reviewResult") + { + Description = "The code review results to evaluate", + IsRequired = true + }, + new KernelParameterMetadata("threshold") + { + Description = "The approval threshold (critical, major, minor, none)", + IsRequired = false, + DefaultValue = "major" + } + }); + } +} diff --git a/PRAgent/Plugins/GitHub/ApprovePRFunction.cs b/PRAgent/Plugins/GitHub/ApprovePRFunction.cs new file mode 100644 index 0000000..11cfd55 --- /dev/null +++ b/PRAgent/Plugins/GitHub/ApprovePRFunction.cs @@ -0,0 +1,111 @@ +using Microsoft.SemanticKernel; +using PRAgent.Services; + +namespace PRAgent.Plugins.GitHub; + +/// +/// Semantic Kernel用のプルリクエスト承認機能プラグイン +/// +public class ApprovePRFunction +{ + private readonly IGitHubService _gitHubService; + private readonly string _owner; + private readonly string _repo; + private readonly int _prNumber; + + public ApprovePRFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + _gitHubService = gitHubService; + _owner = owner; + _repo = repo; + _prNumber = prNumber; + } + + /// + /// プルリクエストを承認します + /// + /// 承認時に追加するコメント(オプション) + /// キャンセレーショントークン + /// 承認結果のメッセージ + [KernelFunction("approve_pull_request")] + public async Task ApproveAsync( + string? comment = null, + CancellationToken cancellationToken = default) + { + try + { + var result = await _gitHubService.ApprovePullRequestAsync(_owner, _repo, _prNumber, comment); + return $"Pull request #{_prNumber} has been approved successfully.{(!string.IsNullOrEmpty(comment) ? $" Comment: {comment}" : "")}"; + } + catch (Exception ex) + { + return $"Failed to approve pull request #{_prNumber}: {ex.Message}"; + } + } + + /// + /// プルリクエストの承認ステータスを確認します + /// + /// キャンセレーショントークン + /// 現在の承認ステータス + [KernelFunction("get_approval_status")] + public async Task GetApprovalStatusAsync( + CancellationToken cancellationToken = default) + { + try + { + var pr = await _gitHubService.GetPullRequestAsync(_owner, _repo, _prNumber); + return $"Pull request #{_prNumber} status: {pr.State}, mergeable: {pr.Mergeable ?? true}"; + } + catch (Exception ex) + { + return $"Failed to get approval status for PR #{_prNumber}: {ex.Message}"; + } + } + + /// + /// KernelFunctionとして使用するためのファクトリメソッド + /// + public static KernelFunction ApproveAsyncFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + var functionPlugin = new ApprovePRFunction(gitHubService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string? comment, CancellationToken ct) => functionPlugin.ApproveAsync(comment, ct), + functionName: "approve_pull_request", + description: "Approves a pull request with an optional comment", + parameters: new[] + { + new KernelParameterMetadata("comment") + { + Description = "Optional comment to add when approving", + DefaultValue = null, + IsRequired = false + } + }); + } + + /// + /// KernelFunctionとして使用するためのファクトリメソッド(ステータス取得) + /// + public static KernelFunction GetApprovalStatusFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + var functionPlugin = new ApprovePRFunction(gitHubService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (CancellationToken ct) => functionPlugin.GetApprovalStatusAsync(ct), + functionName: "get_approval_status", + description: "Gets the current approval status of a pull request" + ); + } +} diff --git a/PRAgent/Plugins/GitHub/PostCommentFunction.cs b/PRAgent/Plugins/GitHub/PostCommentFunction.cs new file mode 100644 index 0000000..2aec491 --- /dev/null +++ b/PRAgent/Plugins/GitHub/PostCommentFunction.cs @@ -0,0 +1,250 @@ +using Microsoft.SemanticKernel; +using Octokit; +using PRAgent.Services; + +namespace PRAgent.Plugins.GitHub; + +/// +/// Semantic Kernel用のプルリクエストコメント投稿機能プラグイン +/// +public class PostCommentFunction +{ + private readonly IGitHubService _gitHubService; + private readonly string _owner; + private readonly string _repo; + private readonly int _prNumber; + + public PostCommentFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + _gitHubService = gitHubService; + _owner = owner; + _repo = repo; + _prNumber = prNumber; + } + + /// + /// プルリクエストに全体コメントを投稿します + /// + /// コメント内容 + /// キャンセレーショントークン + /// 投稿結果のメッセージ + [KernelFunction("post_pr_comment")] + public async Task PostCommentAsync( + string comment, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(comment)) + { + return "Error: Comment cannot be empty"; + } + + try + { + var result = await _gitHubService.CreateIssueCommentAsync(_owner, _repo, _prNumber, comment); + return $"Comment posted successfully to PR #{_prNumber}. Comment ID: {result.Id}"; + } + catch (Exception ex) + { + return $"Failed to post comment to PR #{_prNumber}: {ex.Message}"; + } + } + + /// + /// プルリクエストの特定の行にコメントを投稿します + /// + /// ファイルパス + /// 行番号 + /// コメント内容 + /// 提案される変更内容(オプション) + /// キャンセレーショントークン + /// 投稿結果のメッセージ + [KernelFunction("post_line_comment")] + public async Task PostLineCommentAsync( + string filePath, + int lineNumber, + string comment, + string? suggestion = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + return "Error: File path cannot be empty"; + } + + if (string.IsNullOrWhiteSpace(comment)) + { + return "Error: Comment cannot be empty"; + } + + try + { + var result = await _gitHubService.CreateLineCommentAsync( + _owner, _repo, _prNumber, filePath, lineNumber, comment, suggestion); + return $"Line comment posted successfully to {filePath}:{lineNumber} in PR #{_prNumber}"; + } + catch (Exception ex) + { + return $"Failed to post line comment to PR #{_prNumber}: {ex.Message}"; + } + } + + /// + /// 複数の行コメントを一度に投稿します + /// + /// コメントリスト(ファイルパス、行番号、コメント、提案) + /// キャンセレーショントークン + /// 投稿結果のメッセージ + [KernelFunction("post_multiple_line_comments")] + public async Task PostMultipleLineCommentsAsync( + string commentsJson, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(commentsJson)) + { + return "Error: Comments data cannot be empty"; + } + + try + { + // JSON形式のコメントデータをパース + var comments = System.Text.Json.JsonSerializer.Deserialize>( + commentsJson, + new System.Text.Json.JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (comments == null || comments.Count == 0) + { + return "Error: No valid comments found in the provided data"; + } + + var result = await _gitHubService.CreateMultipleLineCommentsAsync(_owner, _repo, _prNumber, comments); + return $"Successfully posted {comments.Count} line comments to PR #{_prNumber}"; + } + catch (Exception ex) + { + return $"Failed to post multiple line comments to PR #{_prNumber}: {ex.Message}"; + } + } + + /// + /// レビューコメントとして投稿します + /// + /// レビュー本文 + /// キャンセレーショントークン + /// 投稿結果のメッセージ + [KernelFunction("post_review_comment")] + public async Task PostReviewCommentAsync( + string reviewBody, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(reviewBody)) + { + return "Error: Review body cannot be empty"; + } + + try + { + var result = await _gitHubService.CreateReviewCommentAsync(_owner, _repo, _prNumber, reviewBody); + return $"Review comment posted successfully to PR #{_prNumber}. Review ID: {result.Id}"; + } + catch (Exception ex) + { + return $"Failed to post review comment to PR #{_prNumber}: {ex.Message}"; + } + } + + /// + /// KernelFunctionとして使用するためのファクトリメソッド(PRコメント) + /// + public static KernelFunction PostCommentAsyncFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + var functionPlugin = new PostCommentFunction(gitHubService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string comment, CancellationToken ct) => functionPlugin.PostCommentAsync(comment, ct), + functionName: "post_pr_comment", + description: "Posts a general comment to a pull request", + parameters: new[] + { + new KernelParameterMetadata("comment") + { + Description = "The comment content to post", + IsRequired = true + } + }); + } + + /// + /// KernelFunctionとして使用するためのファクトリメソッド(行コメント) + /// + public static KernelFunction PostLineCommentAsyncFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + var functionPlugin = new PostCommentFunction(gitHubService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string filePath, int lineNumber, string comment, string? suggestion, CancellationToken ct) => + functionPlugin.PostLineCommentAsync(filePath, lineNumber, comment, suggestion, ct), + functionName: "post_line_comment", + description: "Posts a comment on a specific line in a pull request file", + parameters: new[] + { + new KernelParameterMetadata("filePath") + { + Description = "The path to the file in the repository", + IsRequired = true + }, + new KernelParameterMetadata("lineNumber") + { + Description = "The line number to comment on", + IsRequired = true + }, + new KernelParameterMetadata("comment") + { + Description = "The comment content", + IsRequired = true + }, + new KernelParameterMetadata("suggestion") + { + Description = "Optional suggestion for the change", + IsRequired = false, + DefaultValue = null + } + }); + } + + /// + /// KernelFunctionとして使用するためのファクトリメソッド(レビューコメント) + /// + public static KernelFunction PostReviewCommentAsyncFunction( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + var functionPlugin = new PostCommentFunction(gitHubService, owner, repo, prNumber); + return KernelFunctionFactory.CreateFromMethod( + (string reviewBody, CancellationToken ct) => functionPlugin.PostReviewCommentAsync(reviewBody, ct), + functionName: "post_review_comment", + description: "Posts a review comment to a pull request", + parameters: new[] + { + new KernelParameterMetadata("reviewBody") + { + Description = "The review content to post", + IsRequired = true + } + }); + } +} diff --git a/PRAgent/Plugins/PRActionFunctions.cs b/PRAgent/Plugins/PRActionFunctions.cs new file mode 100644 index 0000000..f2327dd --- /dev/null +++ b/PRAgent/Plugins/PRActionFunctions.cs @@ -0,0 +1,130 @@ +using Microsoft.SemanticKernel; +using PRAgent.Models; + +namespace PRAgent.Plugins; + +/// +/// PRアクションを蓄積してまとめて投稿するための関数プラグイン +/// エージェントはこれらの関数を呼び出してアクションを追加し、最後にcommit_actionsでまとめて投稿する +/// +public class PRActionFunctions +{ + private readonly PRActionBuffer _buffer; + + public PRActionFunctions(PRActionBuffer buffer) + { + _buffer = buffer; + } + + /// + /// 特定の行にコメントを追加します + /// + [KernelFunction("add_line_comment")] + public string AddLineComment( + string filePath, + int lineNumber, + string comment, + string? suggestion = null) + { + _buffer.AddLineComment(filePath, lineNumber, comment, suggestion); + var suggestionText = !string.IsNullOrEmpty(suggestion) ? $" (提案: {suggestion})" : ""; + return $"行コメントを追加しました: {filePath}:{lineNumber} - {comment}{suggestionText}"; + } + + /// + /// レビューコメントを追加します + /// + [KernelFunction("add_review_comment")] + public string AddReviewComment(string comment) + { + _buffer.AddReviewComment(comment); + return $"レビューコメントを追加しました: {comment}"; + } + + /// + /// サマリーを追加します + /// + [KernelFunction("add_summary")] + public string AddSummary(string summary) + { + _buffer.AddSummary(summary); + return $"サマリーを追加しました: {summary.Substring(0, Math.Min(50, summary.Length))}..."; + } + + /// + /// 全体コメントを設定します + /// + [KernelFunction("set_general_comment")] + public string SetGeneralComment(string comment) + { + _buffer.SetGeneralComment(comment); + return $"全体コメントを設定しました: {comment.Substring(0, Math.Min(50, comment.Length))}..."; + } + + /// + /// 承認をマークします + /// + [KernelFunction("mark_for_approval")] + public string MarkForApproval(string? comment = null) + { + _buffer.MarkForApproval(comment); + var commentText = !string.IsNullOrEmpty(comment) ? $" (コメント: {comment})" : ""; + return $"承認をマークしました{commentText}"; + } + + /// + /// 現在のバッファ状態を取得します + /// + [KernelFunction("get_buffer_state")] + public string GetBufferState() + { + var state = _buffer.GetState(); + return $""" + 現在のバッファ状態: + - 行コメント: {state.LineCommentCount}件 + - レビューコメント: {state.ReviewCommentCount}件 + - サマリー: {state.SummaryCount}件 + - 全体コメント: {(state.HasGeneralComment ? "あり" : "なし")} + - 承認フラグ: {(state.ShouldApprove ? "オン" : "オフ")} + """; + } + + /// + /// バッファをクリアします + /// + [KernelFunction("clear_buffer")] + public string ClearBuffer() + { + _buffer.Clear(); + return "バッファをクリアしました"; + } + + /// + /// アクションのコミット準備が完了したことを示します + /// 実際のコミットは呼び出し元で行います + /// + [KernelFunction("ready_to_commit")] + public string ReadyToCommit() + { + var state = _buffer.GetState(); + var totalActions = state.LineCommentCount + state.ReviewCommentCount + state.SummaryCount + + (state.HasGeneralComment ? 1 : 0) + + (state.ShouldApprove ? 1 : 0); + + if (totalActions == 0) + { + return "コミットするアクションがありません。"; + } + + return $""" + {totalActions}件のアクションをコミット準備完了: + - 行コメント: {state.LineCommentCount}件 + - レビューコメント: {state.ReviewCommentCount}件 + - サマリー: {state.SummaryCount}件 + - 全体コメント: {(state.HasGeneralComment ? "あり" : "なし")} + - 承認: {(state.ShouldApprove ? "あり" : "なし")} + + これらのアクションをGitHubに投稿します。 + """; + } +} diff --git a/PRAgent/Program.cs b/PRAgent/Program.cs index a95e039..5b105eb 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using PRAgent.Agents; +using PRAgent.Agents.SK; using PRAgent.Models; using PRAgent.Services; using PRAgent.Validators; @@ -79,12 +80,19 @@ static async Task Main(string[] args) // Data Services services.AddSingleton(); - // Agents + // Agents (現在稼働中) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - // Agent Orchestrator + // SK Agents (Semantic Kernel Agent Framework) + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Agent Orchestrator (現在は既存の実装を使用) + // TODO: 将来的にSKAgentOrchestratorServiceに切り替え services.AddSingleton(); // PR Analysis Service @@ -130,6 +138,9 @@ static async Task RunCliAsync(string[] args, IServiceProvider services, ICo case "approve": return await RunApproveCommandAsync(args, services); + case "comment": + return await RunCommentCommandAsync(args, services); + case "help": case "--help": case "-h": @@ -163,6 +174,12 @@ static async Task RunReviewCommandAsync(string[] args, IServiceProvider ser return 1; } + if (options.AutoCommit) + { + // Function Callingモードでレビューを実行 + return await RunReviewWithAutoCommitAsync(args, services); + } + var service = services.GetRequiredService(); var review = await service.ReviewPullRequestAsync( options.Owner!, @@ -177,6 +194,78 @@ static async Task RunReviewCommandAsync(string[] args, IServiceProvider ser return 0; } + static async Task RunReviewWithAutoCommitAsync(string[] args, IServiceProvider services) + { + var options = ParseReviewOptions(args); + + // 必要なサービスを取得 + var reviewAgent = services.GetRequiredService(); + var gitHubService = services.GetRequiredService(); + + // バッファを作成 + var buffer = new PRActionBuffer(); + var executor = new PRActionExecutor(gitHubService, options.Owner!, options.Repo!, options.PrNumber); + + Console.WriteLine(); + Console.WriteLine("=== PRレビュー(Function Callingモード) ==="); + Console.WriteLine(); + + // レビューを実行してバッファにアクションを蓄積 + var result = await reviewAgent.ReviewWithActionsAsync( + options.Owner!, + options.Repo!, + options.PrNumber, + buffer); + + Console.WriteLine(result); + Console.WriteLine(); + + // プレビューを表示 + var preview = executor.CreatePreview(buffer); + Console.WriteLine(preview); + Console.WriteLine(); + + // 確認を求める + if (buffer.GetState().LineCommentCount > 0 || + buffer.GetState().ReviewCommentCount > 0 || + buffer.GetState().SummaryCount > 0) + { + Console.Write("上記のアクションを投稿しますか? [投稿する] [キャンセル]: "); + var input = Console.ReadLine()?.Trim().ToLowerInvariant(); + + if (input == "投稿する" || input == "post" || input == "y" || input == "yes") + { + // アクションを実行 + var actionResult = await executor.ExecuteAsync(buffer); + + if (actionResult.Success) + { + Console.WriteLine(); + Console.WriteLine($"✓ {actionResult.Message}"); + if (actionResult.ApprovalUrl != null) + Console.WriteLine($" 承認URL: {actionResult.ApprovalUrl}"); + return 0; + } + else + { + Console.WriteLine(); + Console.WriteLine($"✗ {actionResult.Message}"); + return 1; + } + } + else + { + Console.WriteLine("キャンセルしました。"); + return 0; + } + } + else + { + Console.WriteLine("投稿するアクションがありませんでした。"); + return 0; + } + } + static async Task RunSummaryCommandAsync(string[] args, IServiceProvider services) { var options = ParseSummaryOptions(args); @@ -191,6 +280,12 @@ static async Task RunSummaryCommandAsync(string[] args, IServiceProvider se return 1; } + if (options.AutoCommit) + { + // Function Callingモードでサマリーを作成 + return await RunSummaryWithAutoCommitAsync(args, services); + } + var service = services.GetRequiredService(); var summary = await service.SummarizePullRequestAsync( options.Owner!, @@ -205,6 +300,74 @@ static async Task RunSummaryCommandAsync(string[] args, IServiceProvider se return 0; } + static async Task RunSummaryWithAutoCommitAsync(string[] args, IServiceProvider services) + { + var options = ParseSummaryOptions(args); + + // 必要なサービスを取得 + var summaryAgent = services.GetRequiredService(); + var gitHubService = services.GetRequiredService(); + + // バッファを作成 + var buffer = new PRActionBuffer(); + var executor = new PRActionExecutor(gitHubService, options.Owner!, options.Repo!, options.PrNumber); + + Console.WriteLine(); + Console.WriteLine("=== PRサマリー(Function Callingモード) ==="); + Console.WriteLine(); + + // サマリーを作成してバッファにアクションを蓄積 + var result = await summaryAgent.SummarizeWithActionsAsync( + options.Owner!, + options.Repo!, + options.PrNumber, + buffer); + + Console.WriteLine(result); + Console.WriteLine(); + + // プレビューを表示 + var preview = executor.CreatePreview(buffer); + Console.WriteLine(preview); + Console.WriteLine(); + + // 確認を求める + if (buffer.GetState().SummaryCount > 0 || buffer.GetState().HasGeneralComment) + { + Console.Write("上記のアクションを投稿しますか? [投稿する] [キャンセル]: "); + var input = Console.ReadLine()?.Trim().ToLowerInvariant(); + + if (input == "投稿する" || input == "post" || input == "y" || input == "yes") + { + // アクションを実行 + var actionResult = await executor.ExecuteAsync(buffer); + + if (actionResult.Success) + { + Console.WriteLine(); + Console.WriteLine($"✓ {actionResult.Message}"); + return 0; + } + else + { + Console.WriteLine(); + Console.WriteLine($"✗ {actionResult.Message}"); + return 1; + } + } + else + { + Console.WriteLine("キャンセルしました。"); + return 0; + } + } + else + { + Console.WriteLine("投稿するアクションがありませんでした。"); + return 0; + } + } + static async Task RunApproveCommandAsync(string[] args, IServiceProvider services) { var options = ParseApproveOptions(args); @@ -261,6 +424,147 @@ static async Task RunApproveCommandAsync(string[] args, IServiceProvider se } } + static async Task RunCommentCommandAsync(string[] args, IServiceProvider services) + { + // commentコマンドのみの場合はヘルプを表示 + if (args.Length == 1) + { + Console.WriteLine("コメント投稿機能の使い方:"); + Console.WriteLine(); + Console.WriteLine("基本形式:"); + Console.WriteLine(" comment @123 コメント内容"); + Console.WriteLine(" comment src/file.cs@123 コメント内容"); + Console.WriteLine(); + Console.WriteLine("Suggestion付き:"); + Console.WriteLine(" comment @123 コメント内容 --suggestion \"修正コード\""); + Console.WriteLine(); + Console.WriteLine("承認付き:"); + Console.WriteLine(" comment @123 コメント内容 --approve"); + Console.WriteLine(); + Console.WriteLine("複数コメント:"); + Console.WriteLine(" comment @123 コメント1 @456 コメント2"); + Console.WriteLine(); + Console.WriteLine("行範囲指定:"); + Console.WriteLine(" comment @45-67 このセクション全体を見直してください"); + Console.WriteLine(); + Console.WriteLine("必須オプション:"); + Console.WriteLine(" --owner, -o リポジトリオーナー"); + Console.WriteLine(" --repo, -r リポジトリ名"); + Console.WriteLine(" --pr, -p プルリクエスト番号"); + Console.WriteLine(" --approve コメント投稿後にPRを承認"); + Console.WriteLine(); + Console.WriteLine("例:"); + Console.WriteLine(" PRAgent comment --owner myorg --repo myrepo --pr 123 @150 ここを修正してください"); + Console.WriteLine(" PRAgent comment -o myorg -r myrepo -p 123 @200 不適切なコード --suggestion \"適切なコード\""); + Console.WriteLine(" PRAgent comment --owner myorg --repo myrepo --pr 123 @100 \"修正が必要\" --approve"); + return 0; + } + + var options = CommentCommandOptions.Parse(args); + + if (!options.IsValid(out var errors)) + { + Log.Error("Invalid options:"); + foreach (var error in errors) + { + Log.Error(" - {Error}", error); + } + return 1; + } + + var gitHubService = services.GetRequiredService(); + + 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: c.LineNumber, + 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; + } + } + static ReviewOptions ParseReviewOptions(string[] args) { var options = new ReviewOptions(); @@ -289,6 +593,9 @@ static ReviewOptions ParseReviewOptions(string[] args) case "-c": options = options with { PostComment = true }; break; + case "--auto-commit": + options = options with { AutoCommit = true }; + break; } } @@ -323,6 +630,9 @@ static SummaryOptions ParseSummaryOptions(string[] args) case "-c": options = options with { PostComment = true }; break; + case "--auto-commit": + options = options with { AutoCommit = true }; + break; } } @@ -370,6 +680,9 @@ static ApproveOptions ParseApproveOptions(string[] args) case "-c": options = options with { PostComment = true }; break; + case "--auto-commit": + options = options with { AutoCommit = true }; + break; } } @@ -400,6 +713,7 @@ static void ShowHelp() review Review a pull request summary Summarize a pull request approve Approve a pull request + comment Post a comment on a specific line help Show this help message REVIEW OPTIONS: @@ -423,11 +737,17 @@ help Show this help message --comment, -m Approval comment (only without --auto) --post-comment, -c Post decision as PR comment (only with --auto) - ENVIRONMENT VARIABLES: - AI_ENDPOINT OpenAI-compatible endpoint URL - AI_API_KEY API key for the AI service - AI_MODEL_ID Model ID to use - GITHUB_TOKEN GitHub personal access token + COMMENT OPTIONS: + --owner, -o Repository owner (required) + --repo, -r Repository name (required) + --pr, -p Pull request number (required) + --approve Approve the PR after posting comments + + COMMENT FORMAT: + comment @123 "This needs improvement" + comment @45-67 "Review this section" + comment src/file.cs@123 "Fix this" --suggestion "Fixed code" + comment @123 "Comment1" @456 "Comment2" EXAMPLES: PRAgent review --owner "org" --repo "repo" --pr 123 @@ -436,6 +756,8 @@ PRAgent summary --owner "org" --repo "repo" --pr 123 PRAgent approve --owner "org" --repo "repo" --pr 123 --auto PRAgent approve -o "org" -r "repo" -p 123 --auto --threshold major PRAgent approve --owner "org" --repo "repo" --pr 123 --comment "LGTM" + PRAgent comment --owner "org" --repo "repo" --pr 123 @150 "This part is wrong" --suggestion "This part is correct" + PRAgent comment -o "org" -r "repo" -p 123 @100 "Fix this" --approve """); } @@ -445,6 +767,7 @@ record ReviewOptions public string? Repo { get; init; } public int PrNumber { get; init; } public bool PostComment { get; init; } + public bool AutoCommit { get; init; } public bool IsValid(out List errors) { @@ -467,6 +790,7 @@ record SummaryOptions public string? Repo { get; init; } public int PrNumber { get; init; } public bool PostComment { get; init; } + public bool AutoCommit { get; init; } public bool IsValid(out List errors) { @@ -492,6 +816,7 @@ record ApproveOptions public ApprovalThreshold Threshold { get; init; } = ApprovalThreshold.Minor; public string? Comment { get; init; } public bool PostComment { get; init; } + public bool AutoCommit { get; init; } public bool IsValid(out List errors) { @@ -507,4 +832,4 @@ public bool IsValid(out List errors) return errors.Count == 0; } } -} +} \ No newline at end of file diff --git a/PRAgent/Services/ConfigurationService.cs b/PRAgent/Services/ConfigurationService.cs index f23c16d..9caea2c 100644 --- a/PRAgent/Services/ConfigurationService.cs +++ b/PRAgent/Services/ConfigurationService.cs @@ -1,99 +1,80 @@ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using PRAgent.Configuration; using PRAgent.Models; namespace PRAgent.Services; public class ConfigurationService : IConfigurationService { - private readonly IGitHubService _gitHubService; + private readonly IConfiguration _configuration; private readonly ILogger _logger; public ConfigurationService( - IGitHubService gitHubService, + IConfiguration configuration, ILogger logger) { - _gitHubService = gitHubService; + _configuration = configuration; _logger = logger; } - public async Task GetConfigurationAsync(string owner, string repo, int prNumber) + public Task GetConfigurationAsync(string owner, string repo, int prNumber) { - try + // appsettings.jsonから設定を読み込み + var config = new PRAgentConfig { - // Try to get .github/pragent.yml from the repository - var yamlContent = await _gitHubService.GetRepositoryFileContentAsync(owner, repo, ".github/pragent.yml"); - - if (!string.IsNullOrEmpty(yamlContent)) - { - var config = YamlConfigurationProvider.Deserialize(yamlContent); - if (config?.PRAgent != null) - { - _logger.LogInformation("Loaded PRAgent configuration from .github/pragent.yml"); - return config.PRAgent; - } - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to load .github/pragent.yml, using default configuration"); - } - - // Return default configuration - _logger.LogInformation("Using default PRAgent configuration"); - return GetDefaultConfiguration(); - } - - public async Task GetCustomPromptAsync(string owner, string repo, string promptPath) - { - if (string.IsNullOrEmpty(promptPath)) - { - return null; - } - - try - { - var content = await _gitHubService.GetRepositoryFileContentAsync(owner, repo, promptPath); - return content; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to load custom prompt from {PromptPath}", promptPath); - return null; - } - } - - private static PRAgentConfig GetDefaultConfiguration() - { - return new PRAgentConfig - { - Enabled = true, - SystemPrompt = "You are an expert code reviewer and technical writer.", + Enabled = _configuration.GetValue("PRAgent:Enabled", true), + SystemPrompt = _configuration["PRAgent:SystemPrompt"], Review = new ReviewConfig { - Enabled = true, - AutoPost = false + Enabled = _configuration.GetValue("PRAgent:Review:Enabled", true), + AutoPost = _configuration.GetValue("PRAgent:Review:AutoPost", false), + CustomPrompt = _configuration["PRAgent:Review:CustomPrompt"] }, Summary = new SummaryConfig { - Enabled = true, - PostAsComment = true + Enabled = _configuration.GetValue("PRAgent:Summary:Enabled", true), + PostAsComment = _configuration.GetValue("PRAgent:Summary:PostAsComment", true), + CustomPrompt = _configuration["PRAgent:Summary:CustomPrompt"] }, Approve = new ApproveConfig { - Enabled = true, - AutoApproveThreshold = "minor", - RequireReviewFirst = true + Enabled = _configuration.GetValue("PRAgent:Approve:Enabled", true), + AutoApproveThreshold = _configuration.GetValue("PRAgent:Approve:AutoApproveThreshold", "minor") ?? "minor", + RequireReviewFirst = _configuration.GetValue("PRAgent:Approve:RequireReviewFirst", true) }, - IgnorePaths = new List + IgnorePaths = _configuration.GetSection("PRAgent:IgnorePaths").Get>() ?? GetDefaultIgnorePaths(), + AgentFramework = new AgentFrameworkConfig { - "*.min.js", - "dist/**", - "node_modules/**", - "*.min.css", - "bin/**", - "obj/**" + Enabled = _configuration.GetValue("PRAgent:AgentFramework:Enabled", false), + OrchestrationMode = _configuration.GetValue("PRAgent:AgentFramework:OrchestrationMode", "sequential") ?? "sequential", + SelectionStrategy = _configuration.GetValue("PRAgent:AgentFramework:SelectionStrategy", "approval_workflow") ?? "approval_workflow", + EnableFunctionCalling = _configuration.GetValue("PRAgent:AgentFramework:EnableFunctionCalling", true), + EnableAutoApproval = _configuration.GetValue("PRAgent:AgentFramework:EnableAutoApproval", false), + MaxTurns = _configuration.GetValue("PRAgent:AgentFramework:MaxTurns", 10) } }; + + _logger.LogInformation("Loaded PRAgent configuration from appsettings.json"); + return Task.FromResult(config); + } + + public Task GetCustomPromptAsync(string owner, string repo, string promptPath) + { + // appsettings.jsonからカスタムプロンプトを取得 + var customPrompt = _configuration[$"PRAgent:CustomPrompts:{promptPath}"]; + return Task.FromResult(customPrompt); + } + + private static List GetDefaultIgnorePaths() + { + return new List + { + "*.min.js", + "dist/**", + "node_modules/**", + "*.min.css", + "bin/**", + "obj/**" + }; } } diff --git a/PRAgent/Services/GitHubService.cs b/PRAgent/Services/GitHubService.cs index 50a0df7..78464c5 100644 --- a/PRAgent/Services/GitHubService.cs +++ b/PRAgent/Services/GitHubService.cs @@ -125,4 +125,44 @@ public async Task FileExistsAsync(string owner, string repo, string path, var content = await GetRepositoryFileContentAsync(owner, repo, path, branch); return content != null; } + + public async Task CreateLineCommentAsync(string owner, string repo, int prNumber, string filePath, int lineNumber, string comment, string? suggestion = null) + { + // 行コメントを作成 + var commentBody = suggestion != null ? $"{comment}\n```suggestion\n{suggestion}\n```" : comment; + + return await _client.PullRequest.Review.Create( + owner, + repo, + prNumber, + new PullRequestReviewCreate + { + Event = PullRequestReviewEvent.Comment, + Comments = new List + { + new DraftPullRequestReviewComment(commentBody, filePath, lineNumber) + } + } + ); + } + + public async Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int LineNumber, string Comment, string? Suggestion)> comments) + { + var draftComments = comments.Select(c => + { + var commentBody = c.Suggestion != null ? $"{c.Comment}\n```suggestion\n{c.Suggestion}\n```" : c.Comment; + return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.LineNumber); + }).ToList(); + + return await _client.PullRequest.Review.Create( + owner, + repo, + prNumber, + new PullRequestReviewCreate + { + Event = PullRequestReviewEvent.Comment, + Comments = draftComments + } + ); + } } diff --git a/PRAgent/Services/IGitHubService.cs b/PRAgent/Services/IGitHubService.cs index 7315e10..587a56a 100644 --- a/PRAgent/Services/IGitHubService.cs +++ b/PRAgent/Services/IGitHubService.cs @@ -10,6 +10,8 @@ public interface IGitHubService Task> GetPullRequestReviewCommentsAsync(string owner, string repo, int prNumber); Task GetPullRequestDiffAsync(string owner, string repo, int prNumber); Task CreateReviewCommentAsync(string owner, string repo, int prNumber, string body); + Task CreateLineCommentAsync(string owner, string repo, int prNumber, string filePath, int lineNumber, string comment, string? suggestion = null); + Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int LineNumber, string Comment, string? Suggestion)> comments); Task CreateIssueCommentAsync(string owner, string repo, int prNumber, string body); Task ApprovePullRequestAsync(string owner, string repo, int prNumber, string? comment = null); Task GetRepositoryFileContentAsync(string owner, string repo, string path, string? branch = null); diff --git a/PRAgent/Services/IKernelService.cs b/PRAgent/Services/IKernelService.cs index ed5d2b5..d31653d 100644 --- a/PRAgent/Services/IKernelService.cs +++ b/PRAgent/Services/IKernelService.cs @@ -5,6 +5,9 @@ namespace PRAgent.Services; public interface IKernelService { Kernel CreateKernel(string? systemPrompt = null); + Kernel CreateAgentKernel(string? systemPrompt = null); + Kernel RegisterFunctionPlugins(Kernel kernel, IEnumerable plugins); + Kernel RegisterFunctionPlugin(Kernel kernel, object plugin, string? pluginName = null); IAsyncEnumerable InvokePromptAsync(Kernel kernel, string prompt, CancellationToken cancellationToken = default); Task InvokePromptAsStringAsync(Kernel kernel, string prompt, CancellationToken cancellationToken = default); } diff --git a/PRAgent/Services/KernelService.cs b/PRAgent/Services/KernelService.cs index c44fc15..131aa15 100644 --- a/PRAgent/Services/KernelService.cs +++ b/PRAgent/Services/KernelService.cs @@ -29,6 +29,52 @@ public Kernel CreateKernel(string? systemPrompt = null) return kernel; } + public Kernel CreateAgentKernel(string? systemPrompt = null) + { + var builder = Kernel.CreateBuilder(); + + builder.AddOpenAIChatCompletion( + modelId: _aiSettings.ModelId, + apiKey: _aiSettings.ApiKey, + endpoint: new Uri(_aiSettings.Endpoint) + ); + + var kernel = builder.Build(); + + // 注: SetDefaultSystemPromptは現在のバージョンではまだ利用できない + // 将来的には以下のようにsystemPromptを設定できるようになる予定 + // if (!string.IsNullOrEmpty(systemPrompt)) + // { + // kernel.SetDefaultSystemPrompt(systemPrompt); + // } + + return kernel; + } + + public Kernel RegisterFunctionPlugins(Kernel kernel, IEnumerable plugins) + { + foreach (var plugin in plugins) + { + kernel.ImportPluginFromObject(plugin); + } + + return kernel; + } + + public Kernel RegisterFunctionPlugin(Kernel kernel, object plugin, string? pluginName = null) + { + if (!string.IsNullOrEmpty(pluginName)) + { + kernel.ImportPluginFromObject(plugin, pluginName); + } + else + { + kernel.ImportPluginFromObject(plugin); + } + + return kernel; + } + public async IAsyncEnumerable InvokePromptAsync( Kernel kernel, string prompt, diff --git a/PRAgent/Services/PRActionExecutor.cs b/PRAgent/Services/PRActionExecutor.cs new file mode 100644 index 0000000..5d2bf0a --- /dev/null +++ b/PRAgent/Services/PRActionExecutor.cs @@ -0,0 +1,185 @@ +using Octokit; +using PRAgent.Models; +using PRAgent.Services; + +namespace PRAgent.Services; + +/// +/// 蓄積されたPRアクションをGitHubに投稿するサービス +/// +public class PRActionExecutor +{ + private readonly IGitHubService _gitHubService; + private readonly string _owner; + private readonly string _repo; + private readonly int _prNumber; + + public PRActionExecutor( + IGitHubService gitHubService, + string owner, + string repo, + int prNumber) + { + _gitHubService = gitHubService; + _owner = owner; + _repo = repo; + _prNumber = prNumber; + } + + /// + /// バッファ内のすべてのアクションをGitHubに投稿します + /// + public async Task ExecuteAsync(PRActionBuffer buffer, CancellationToken cancellationToken = default) + { + var result = new PRActionResult + { + Owner = _owner, + Repo = _repo, + PrNumber = _prNumber + }; + + try + { + // 1. 行コメントを投稿 + if (buffer.LineComments.Count > 0) + { + var comments = buffer.LineComments.Select(c => ( + c.FilePath, + c.LineNumber, + c.Comment, + c.Suggestion + )).ToList(); + + var reviewResult = await _gitHubService.CreateMultipleLineCommentsAsync( + _owner, _repo, _prNumber, comments); + + result.LineCommentsPosted = comments.Count; + result.Success = true; + } + + // 2. サマリーを全体コメントとして投稿 + if (buffer.Summaries.Count > 0) + { + var summaryText = string.Join("\n\n", buffer.Summaries); + var commentResult = await _gitHubService.CreateIssueCommentAsync( + _owner, _repo, _prNumber, + $@"## PR Summary + +{summaryText}"); + + result.SummariesPosted = buffer.Summaries.Count; + result.SummaryCommentUrl = commentResult.HtmlUrl; + } + + // 3. 全体コメントを投稿 + if (!string.IsNullOrEmpty(buffer.GeneralComment)) + { + var commentResult = await _gitHubService.CreateIssueCommentAsync( + _owner, _repo, _prNumber, buffer.GeneralComment); + + result.GeneralCommentPosted = true; + result.GeneralCommentUrl = commentResult.HtmlUrl; + } + + // 4. 承認を実行 + if (buffer.ShouldApprove) + { + var approvalResult = await _gitHubService.ApprovePullRequestAsync( + _owner, _repo, _prNumber, buffer.ApprovalComment); + + result.Approved = true; + result.ApprovalUrl = approvalResult.HtmlUrl; + } + + result.TotalActionsPosted = + result.LineCommentsPosted + + result.SummariesPosted + + (result.GeneralCommentPosted ? 1 : 0) + + (result.Approved ? 1 : 0); + + result.Message = $"Successfully posted {result.TotalActionsPosted} action(s) to PR #{_prNumber}"; + } + catch (Exception ex) + { + result.Success = false; + result.Error = ex.Message; + result.Message = $"Failed to post actions: {ex.Message}"; + } + + return result; + } + + /// + /// アクションをGitHubに投稿する前に確認するためのサマリーを作成します + /// + public string CreatePreview(PRActionBuffer buffer) + { + var preview = $""" + ## PR # {_prNumber} に投稿されるアクションのプレビュー + + """; + + if (buffer.LineComments.Count > 0) + { + preview += $"### 行コメント ({buffer.LineComments.Count}件)\n"; + foreach (var comment in buffer.LineComments) + { + var suggestion = !string.IsNullOrEmpty(comment.Suggestion) + ? $"\n 提案: {comment.Suggestion}" + : ""; + preview += $"- {comment.FilePath}:{comment.LineNumber}: {comment.Comment}{suggestion}\n"; + } + preview += "\n"; + } + + if (buffer.Summaries.Count > 0) + { + preview += $"### サマリー ({buffer.Summaries.Count}件)\n"; + foreach (var summary in buffer.Summaries) + { + preview += $"- {summary.Substring(0, Math.Min(100, summary.Length))}...\n"; + } + preview += "\n"; + } + + if (!string.IsNullOrEmpty(buffer.GeneralComment)) + { + preview += $"### 全体コメント\n{buffer.GeneralComment.Substring(0, Math.Min(200, buffer.GeneralComment.Length))}...\n\n"; + } + + if (buffer.ShouldApprove) + { + preview += $"### 承認\nはい - {buffer.ApprovalComment ?? "コメントなし"}\n\n"; + } + + var totalActions = buffer.LineComments.Count + + buffer.Summaries.Count + + (string.IsNullOrEmpty(buffer.GeneralComment) ? 0 : 1) + + (buffer.ShouldApprove ? 1 : 0); + + preview += $"**合計: {totalActions}件のアクション**"; + + return preview; + } +} + +/// +/// PRアクションの実行結果 +/// +public class PRActionResult +{ + public string Owner { get; init; } = string.Empty; + public string Repo { get; init; } = string.Empty; + public int PrNumber { get; init; } + public bool Success { get; set; } + public int TotalActionsPosted { get; set; } + public int LineCommentsPosted { get; set; } + public int SummariesPosted { get; set; } + public bool GeneralCommentPosted { get; set; } + public bool Approved { get; set; } + public string? SummaryCommentUrl { get; set; } + public string? GeneralCommentUrl { get; set; } + public string? ApprovalUrl { get; set; } + public string? Error { get; set; } + public string Message { get; set; } = string.Empty; +} diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs new file mode 100644 index 0000000..ea28b8f --- /dev/null +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -0,0 +1,262 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.ChatCompletion; +using PRAgent.Agents; +using PRAgent.Agents.SK; +using PRAgent.Models; +using PRAgent.Plugins.Agent; +using PRAgent.Plugins.GitHub; +using PRAgentDefinition = PRAgent.Agents.AgentDefinition; + +namespace PRAgent.Services.SK; + +/// +/// Semantic Kernel AgentGroupChatを使用したエージェントオーケストレーションサービス +/// +public class SKAgentOrchestratorService : IAgentOrchestratorService +{ + private readonly PRAgentFactory _agentFactory; + private readonly SKReviewAgent _reviewAgent; + private readonly SKSummaryAgent _summaryAgent; + private readonly SKApprovalAgent _approvalAgent; + private readonly IGitHubService _gitHubService; + private readonly PullRequestDataService _prDataService; + + public SKAgentOrchestratorService( + PRAgentFactory agentFactory, + SKReviewAgent reviewAgent, + SKSummaryAgent summaryAgent, + SKApprovalAgent approvalAgent, + IGitHubService gitHubService, + PullRequestDataService prDataService) + { + _agentFactory = agentFactory; + _reviewAgent = reviewAgent; + _summaryAgent = summaryAgent; + _approvalAgent = approvalAgent; + _gitHubService = gitHubService; + _prDataService = prDataService; + } + + /// + /// プルリクエストのコードレビューを実行します + /// + public async Task ReviewAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default) + { + return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + } + + /// + /// プルリクエストの要約を作成します + /// + public async Task SummarizeAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default) + { + return await _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + } + + /// + /// レビューと承認を一連のワークフローとして実行します + /// + public async Task ReviewAndApproveAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + CancellationToken cancellationToken = default) + { + // ワークフロー: ReviewAgent → ApprovalAgent + var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + + var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( + owner, repo, prNumber, review, threshold, cancellationToken); + + string? approvalUrl = null; + if (shouldApprove) + { + var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); + approvalUrl = result; + } + + return new ApprovalResult + { + Approved = shouldApprove, + Review = review, + Reasoning = reasoning, + Comment = comment, + ApprovalUrl = approvalUrl + }; + } + + /// + /// AgentGroupChatを使用したマルチエージェント協調によるレビューと承認 + /// 注: 現在はAgentGroupChat APIの変更により、基本実装を使用しています + /// + public async Task ReviewAndApproveWithAgentChatAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + CancellationToken cancellationToken = default) + { + // 現在は基本ワークフローを使用 + // TODO: AgentGroupChat APIが安定したら実装 + return await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken); + } + + /// + /// カスタムワークフローを使用したレビューと承認 + /// + public async Task ReviewAndApproveWithCustomWorkflowAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + string workflow, + CancellationToken cancellationToken = default) + { + return workflow.ToLower() switch + { + "collaborative" => await CollaborativeReviewWorkflowAsync(owner, repo, prNumber, threshold, cancellationToken), + "parallel" => await ParallelReviewWorkflowAsync(owner, repo, prNumber, threshold, cancellationToken), + "sequential" => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken), + _ => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken) + }; + } + + /// + /// 協調型レビューワークフロー - SummaryとReviewの両方を実行 + /// + private async Task CollaborativeReviewWorkflowAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + CancellationToken cancellationToken) + { + // SummaryとReviewを並行して実行 + var summaryTask = _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + var reviewTask = _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + + await Task.WhenAll(summaryTask, reviewTask); + + var summary = await summaryTask; + var review = await reviewTask; + + // 要約を含むレビュー結果を作成 + var enhancedReview = $""" + {review} + + ## Summary + {summary} + """; + + var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( + owner, repo, prNumber, enhancedReview, threshold, cancellationToken); + + string? approvalUrl = null; + if (shouldApprove) + { + var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); + approvalUrl = result; + } + + return new ApprovalResult + { + Approved = shouldApprove, + Review = enhancedReview, + Reasoning = reasoning, + Comment = comment, + ApprovalUrl = approvalUrl + }; + } + + /// + /// 並列レビューワークフロー - 複数の視点からレビュー + /// + private async Task ParallelReviewWorkflowAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + CancellationToken cancellationToken) + { + // 様々な視点でレビューを実行 + var securityReviewTask = _reviewAgent.ReviewAsync( + owner, repo, prNumber, + "Focus specifically on security vulnerabilities, authentication issues, and data protection concerns.", + cancellationToken); + + var performanceReviewTask = _reviewAgent.ReviewAsync( + owner, repo, prNumber, + "Focus specifically on performance implications, scalability concerns, and resource usage.", + cancellationToken); + + var codeQualityReviewTask = _reviewAgent.ReviewAsync( + owner, repo, prNumber, + "Focus specifically on code quality, maintainability, and adherence to best practices.", + cancellationToken); + + await Task.WhenAll(securityReviewTask, performanceReviewTask, codeQualityReviewTask); + + var securityReview = await securityReviewTask; + var performanceReview = await performanceReviewTask; + var codeQualityReview = await codeQualityReviewTask; + + // 統合レビューを作成 + var combinedReview = $""" + ## Combined Code Review + + ### Security Review + {securityReview} + + ### Performance Review + {performanceReview} + + ### Code Quality Review + {codeQualityReview} + """; + + var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( + owner, repo, prNumber, combinedReview, threshold, cancellationToken); + + string? approvalUrl = null; + if (shouldApprove) + { + var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); + approvalUrl = result; + } + + return new ApprovalResult + { + Approved = shouldApprove, + Review = combinedReview, + Reasoning = reasoning, + Comment = comment, + ApprovalUrl = approvalUrl + }; + } + + /// + /// 関数呼び出し機能を持つApprovalエージェントを作成 + /// + private async Task CreateApprovalAgentWithFunctionsAsync( + string owner, string repo, int prNumber) + { + // GitHub操作用のプラグインを作成 + var approveFunction = new ApprovePRFunction(_gitHubService, owner, repo, prNumber); + var commentFunction = new PostCommentFunction(_gitHubService, owner, repo, prNumber); + + // 関数をKernelFunctionとして登録 + var functions = new List + { + ApprovePRFunction.ApproveAsyncFunction(_gitHubService, owner, repo, prNumber), + ApprovePRFunction.GetApprovalStatusFunction(_gitHubService, owner, repo, prNumber), + PostCommentFunction.PostCommentAsyncFunction(_gitHubService, owner, repo, prNumber), + PostCommentFunction.PostLineCommentAsyncFunction(_gitHubService, owner, repo, prNumber) + }; + + return await _agentFactory.CreateApprovalAgentAsync( + owner, repo, prNumber, null, functions); + } +} diff --git a/PRAgent/appsettings.Development.json b/PRAgent/appsettings.Development.json new file mode 100644 index 0000000..3aac74b --- /dev/null +++ b/PRAgent/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AISettings": { + "Endpoint": "https://api.openai.com/v1", + "ApiKey": "", + "ModelId": "gpt-4o-mini", + "Language": "ja" + }, + "PRSettings": { + "GitHubToken": "", + "DefaultOwner": "", + "DefaultRepo": "" + } +} \ No newline at end of file diff --git a/PRAgent/appsettings.json b/PRAgent/appsettings.json index 749497a..f5ae172 100644 --- a/PRAgent/appsettings.json +++ b/PRAgent/appsettings.json @@ -11,6 +11,41 @@ "DefaultOwner": "", "DefaultRepo": "" }, + "PRAgent": { + "Enabled": true, + "SystemPrompt": "You are an expert code reviewer and technical writer.", + "Review": { + "Enabled": true, + "AutoPost": false, + "CustomPrompt": null + }, + "Summary": { + "Enabled": true, + "PostAsComment": true, + "CustomPrompt": null + }, + "Approve": { + "Enabled": true, + "AutoApproveThreshold": "minor", + "RequireReviewFirst": true + }, + "IgnorePaths": [ + "*.min.js", + "dist/**", + "node_modules/**", + "*.min.css", + "bin/**", + "obj/**" + ], + "AgentFramework": { + "Enabled": false, + "OrchestrationMode": "sequential", + "SelectionStrategy": "approval_workflow", + "EnableFunctionCalling": true, + "EnableAutoApproval": false, + "MaxTurns": 10 + } + }, "Logging": { "LogLevel": { "Default": "Information", From ef210d2d176bac24db9ebe9a1d49d9247a347880 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:15:00 +0900 Subject: [PATCH 02/27] [fix] pragent-native --- .github/workflows/pragent-native.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) 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: From 0838273005a11ed1a70993097e35fd165cd86873 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:25:53 +0900 Subject: [PATCH 03/27] [fix] --- PRAgent/Program.cs | 20 ++++++- .../Services/SK/SKAgentOrchestratorService.cs | 60 +++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/PRAgent/Program.cs b/PRAgent/Program.cs index 5b105eb..2a9b4d6 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -5,6 +5,7 @@ using PRAgent.Agents.SK; using PRAgent.Models; using PRAgent.Services; +using PRAgent.Services.SK; using PRAgent.Validators; using Serilog; @@ -68,6 +69,11 @@ static async Task Main(string[] args) services.AddSingleton(_ => aiSettings); services.AddSingleton(_ => prSettings); + // PRAgent Configuration + var prAgentConfig = configuration.GetSection("PRAgent").Get() + ?? new PRAgentConfig(); + services.AddSingleton(_ => prAgentConfig); + // GitHub Service services.AddSingleton(); @@ -91,9 +97,17 @@ static async Task Main(string[] args) services.AddSingleton(); services.AddSingleton(); - // Agent Orchestrator (現在は既存の実装を使用) - // TODO: 将来的にSKAgentOrchestratorServiceに切り替え - services.AddSingleton(); + // Agent Orchestrator - AgentFramework設定に応じて切り替え + if (prAgentConfig.AgentFramework?.Enabled == true) + { + services.AddSingleton(); + Log.Information("Using SKAgentOrchestratorService (Agent Framework enabled)"); + } + else + { + services.AddSingleton(); + Log.Information("Using AgentOrchestratorService (standard mode)"); + } // PR Analysis Service services.AddSingleton(); diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index ea28b8f..1d61d48 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; @@ -22,6 +23,8 @@ public class SKAgentOrchestratorService : IAgentOrchestratorService private readonly SKApprovalAgent _approvalAgent; private readonly IGitHubService _gitHubService; private readonly PullRequestDataService _prDataService; + private readonly PRAgentConfig _config; + private readonly ILogger _logger; public SKAgentOrchestratorService( PRAgentFactory agentFactory, @@ -29,7 +32,9 @@ public SKAgentOrchestratorService( SKSummaryAgent summaryAgent, SKApprovalAgent approvalAgent, IGitHubService gitHubService, - PullRequestDataService prDataService) + PullRequestDataService prDataService, + PRAgentConfig config, + ILogger logger) { _agentFactory = agentFactory; _reviewAgent = reviewAgent; @@ -37,6 +42,8 @@ public SKAgentOrchestratorService( _approvalAgent = approvalAgent; _gitHubService = gitHubService; _prDataService = prDataService; + _config = config; + _logger = logger; } /// @@ -90,7 +97,6 @@ public async Task ReviewAndApproveAsync( /// /// AgentGroupChatを使用したマルチエージェント協調によるレビューと承認 - /// 注: 現在はAgentGroupChat APIの変更により、基本実装を使用しています /// public async Task ReviewAndApproveWithAgentChatAsync( string owner, @@ -99,8 +105,29 @@ public async Task ReviewAndApproveWithAgentChatAsync( ApprovalThreshold threshold, CancellationToken cancellationToken = default) { - // 現在は基本ワークフローを使用 - // TODO: AgentGroupChat APIが安定したら実装 + // 設定されたオーケストレーションモードに応じて処理を分岐 + var orchestrationMode = _config.AgentFramework?.OrchestrationMode ?? "sequential"; + + return orchestrationMode.ToLower() switch + { + "agent_chat" => await ExecuteWithAgentGroupChatAsync(owner, repo, prNumber, threshold, cancellationToken), + "sequential" => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken), + _ => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken) + }; + } + + /// + /// AgentGroupChatを使用した実行 + /// 注: Semantic Kernel 1.68.0のAgentGroupChat APIは複雑なため、簡易実装としています + /// + private async Task ExecuteWithAgentGroupChatAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + CancellationToken cancellationToken) + { + // 現在はSequentialモードとして実行(将来的に完全なAgentGroupChatを実装) return await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken); } @@ -243,6 +270,9 @@ private async Task ParallelReviewWorkflowAsync( private async Task CreateApprovalAgentWithFunctionsAsync( string owner, string repo, int prNumber) { + // 設定を反映したカスタムプロンプトを作成 + var customPrompt = _config.AgentFramework?.Agents?.Approval?.CustomSystemPrompt; + // GitHub操作用のプラグインを作成 var approveFunction = new ApprovePRFunction(_gitHubService, owner, repo, prNumber); var commentFunction = new PostCommentFunction(_gitHubService, owner, repo, prNumber); @@ -257,6 +287,26 @@ private async Task CreateApprovalAgentWithFunctionsAsync( }; return await _agentFactory.CreateApprovalAgentAsync( - owner, repo, prNumber, null, functions); + owner, repo, prNumber, customPrompt, functions); + } + + /// + /// 設定を反映したReviewエージェントを作成 + /// + private async Task CreateConfiguredReviewAgentAsync( + string owner, string repo, int prNumber) + { + var customPrompt = _config.AgentFramework?.Agents?.Review?.CustomSystemPrompt; + return await _agentFactory.CreateReviewAgentAsync(owner, repo, prNumber, customPrompt); + } + + /// + /// 設定を反映したSummaryエージェントを作成 + /// + private async Task CreateConfiguredSummaryAgentAsync( + string owner, string repo, int prNumber) + { + var customPrompt = _config.AgentFramework?.Agents?.Summary?.CustomSystemPrompt; + return await _agentFactory.CreateSummaryAgentAsync(owner, repo, prNumber, customPrompt); } } From e048554695b4c940544160f004e8effb81ff4191 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:36:53 +0900 Subject: [PATCH 04/27] [fix] error --- PRAgent/Agents/ReviewAgent.cs | 1 + PRAgent/CommandLine/CommentCommandHandler.cs | 123 ++++ PRAgent/Program.cs | 660 +----------------- .../Services/SK/SKAgentOrchestratorService.cs | 33 + 4 files changed, 167 insertions(+), 650 deletions(-) create mode 100644 PRAgent/CommandLine/CommentCommandHandler.cs diff --git a/PRAgent/Agents/ReviewAgent.cs b/PRAgent/Agents/ReviewAgent.cs index 1c195be..4a60152 100644 --- a/PRAgent/Agents/ReviewAgent.cs +++ b/PRAgent/Agents/ReviewAgent.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using PRAgent.Models; diff --git a/PRAgent/CommandLine/CommentCommandHandler.cs b/PRAgent/CommandLine/CommentCommandHandler.cs new file mode 100644 index 0000000..40aae95 --- /dev/null +++ b/PRAgent/CommandLine/CommentCommandHandler.cs @@ -0,0 +1,123 @@ +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: c.LineNumber, + 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/Program.cs b/PRAgent/Program.cs index 1edaf96..dda9e91 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting; using PRAgent.Agents; using PRAgent.Agents.SK; +using PRAgent.CommandLine; using PRAgent.Models; using PRAgent.Services; using PRAgent.Services.SK; @@ -145,6 +146,7 @@ static async Task RunCliAsync(string[] args, IServiceProvider services) "review" => CreateReviewHandler(args, services), "summary" => CreateSummaryHandler(args, services), "approve" => CreateApproveHandler(args, services), + "comment" => CreateCommentHandler(args, services), "help" or "--help" or "-h" => null, _ => null }; @@ -154,18 +156,6 @@ static async Task RunCliAsync(string[] args, IServiceProvider services) return await commandHandler.ExecuteAsync(); } - case "approve": - return await RunApproveCommandAsync(args, services); - - case "comment": - return await RunCommentCommandAsync(args, services); - - case "help": - case "--help": - case "-h": - ShowHelp(); - return 0; - Log.Error("Unknown command: {Command}", command); HelpTextGenerator.ShowHelp(); return 1; @@ -184,655 +174,25 @@ private static ICommandHandler CreateReviewHandler(string[] args, IServiceProvid return new ReviewCommandHandler(options, prAnalysisService); } - if (options.AutoCommit) - { - // Function Callingモードでレビューを実行 - return await RunReviewWithAutoCommitAsync(args, services); - } - - var service = services.GetRequiredService(); - var review = await service.ReviewPullRequestAsync( - options.Owner!, - options.Repo!, - options.PrNumber, - options.PostComment); - - Console.WriteLine(); - Console.WriteLine(review); - Console.WriteLine(); - - return 0; - } - - static async Task RunReviewWithAutoCommitAsync(string[] args, IServiceProvider services) - { - var options = ParseReviewOptions(args); - - // 必要なサービスを取得 - var reviewAgent = services.GetRequiredService(); - var gitHubService = services.GetRequiredService(); - - // バッファを作成 - var buffer = new PRActionBuffer(); - var executor = new PRActionExecutor(gitHubService, options.Owner!, options.Repo!, options.PrNumber); - - Console.WriteLine(); - Console.WriteLine("=== PRレビュー(Function Callingモード) ==="); - Console.WriteLine(); - - // レビューを実行してバッファにアクションを蓄積 - var result = await reviewAgent.ReviewWithActionsAsync( - options.Owner!, - options.Repo!, - options.PrNumber, - buffer); - - Console.WriteLine(result); - Console.WriteLine(); - - // プレビューを表示 - var preview = executor.CreatePreview(buffer); - Console.WriteLine(preview); - Console.WriteLine(); - - // 確認を求める - if (buffer.GetState().LineCommentCount > 0 || - buffer.GetState().ReviewCommentCount > 0 || - buffer.GetState().SummaryCount > 0) - { - Console.Write("上記のアクションを投稿しますか? [投稿する] [キャンセル]: "); - var input = Console.ReadLine()?.Trim().ToLowerInvariant(); - - if (input == "投稿する" || input == "post" || input == "y" || input == "yes") - { - // アクションを実行 - var actionResult = await executor.ExecuteAsync(buffer); - - if (actionResult.Success) - { - Console.WriteLine(); - Console.WriteLine($"✓ {actionResult.Message}"); - if (actionResult.ApprovalUrl != null) - Console.WriteLine($" 承認URL: {actionResult.ApprovalUrl}"); - return 0; - } - else - { - Console.WriteLine(); - Console.WriteLine($"✗ {actionResult.Message}"); - return 1; - } - } - else - { - Console.WriteLine("キャンセルしました。"); - return 0; - } - } - else - { - Console.WriteLine("投稿するアクションがありませんでした。"); - return 0; - } - } - - static async Task RunSummaryCommandAsync(string[] args, IServiceProvider services) + private static ICommandHandler CreateSummaryHandler(string[] args, IServiceProvider services) { var options = CommandLineParser.ParseSummaryOptions(args); var prAnalysisService = services.GetRequiredService(); return new SummaryCommandHandler(options, prAnalysisService); } - if (options.AutoCommit) - { - // Function Callingモードでサマリーを作成 - return await RunSummaryWithAutoCommitAsync(args, services); - } - - var service = services.GetRequiredService(); - var summary = await service.SummarizePullRequestAsync( - options.Owner!, - options.Repo!, - options.PrNumber, - options.PostComment); - - Console.WriteLine(); - Console.WriteLine(summary); - Console.WriteLine(); - - return 0; - } - - static async Task RunSummaryWithAutoCommitAsync(string[] args, IServiceProvider services) + private static ICommandHandler CreateApproveHandler(string[] args, IServiceProvider services) { - var options = ParseSummaryOptions(args); - - // 必要なサービスを取得 - var summaryAgent = services.GetRequiredService(); + var options = CommandLineParser.ParseApproveOptions(args); + var prAnalysisService = services.GetRequiredService(); var gitHubService = services.GetRequiredService(); - - // バッファを作成 - var buffer = new PRActionBuffer(); - var executor = new PRActionExecutor(gitHubService, options.Owner!, options.Repo!, options.PrNumber); - - Console.WriteLine(); - Console.WriteLine("=== PRサマリー(Function Callingモード) ==="); - Console.WriteLine(); - - // サマリーを作成してバッファにアクションを蓄積 - var result = await summaryAgent.SummarizeWithActionsAsync( - options.Owner!, - options.Repo!, - options.PrNumber, - buffer); - - Console.WriteLine(result); - Console.WriteLine(); - - // プレビューを表示 - var preview = executor.CreatePreview(buffer); - Console.WriteLine(preview); - Console.WriteLine(); - - // 確認を求める - if (buffer.GetState().SummaryCount > 0 || buffer.GetState().HasGeneralComment) - { - Console.Write("上記のアクションを投稿しますか? [投稿する] [キャンセル]: "); - var input = Console.ReadLine()?.Trim().ToLowerInvariant(); - - if (input == "投稿する" || input == "post" || input == "y" || input == "yes") - { - // アクションを実行 - var actionResult = await executor.ExecuteAsync(buffer); - - if (actionResult.Success) - { - Console.WriteLine(); - Console.WriteLine($"✓ {actionResult.Message}"); - return 0; - } - else - { - Console.WriteLine(); - Console.WriteLine($"✗ {actionResult.Message}"); - return 1; - } - } - else - { - Console.WriteLine("キャンセルしました。"); - return 0; - } - } - else - { - Console.WriteLine("投稿するアクションがありませんでした。"); - return 0; - } - } - - static async Task RunApproveCommandAsync(string[] args, IServiceProvider services) - { - var options = ParseApproveOptions(args); - - if (!options.IsValid(out var errors)) - { - Log.Error("Invalid options:"); - foreach (var error in errors) - { - Log.Error(" - {Error}", error); - } - return 1; - } - - var service = services.GetRequiredService(); - - if (options.Auto) - { - // Auto mode: Review and approve based on AI decision - var result = await service.ReviewAndApproveAsync( - options.Owner!, - options.Repo!, - options.PrNumber, - options.Threshold, - options.PostComment); - - Console.WriteLine(); - Console.WriteLine("## Review Result"); - Console.WriteLine(result.Review); - Console.WriteLine(); - Console.WriteLine($"## Approval Decision: {(result.Approved ? "APPROVED" : "NOT APPROVED")}"); - Console.WriteLine($"Reasoning: {result.Reasoning}"); - - if (result.Approved && !string.IsNullOrEmpty(result.ApprovalUrl)) - { - Console.WriteLine($"Approval URL: {result.ApprovalUrl}"); - } - Console.WriteLine(); - - return result.Approved ? 0 : 1; - } - else - { - // Direct approval without review - var gitHubService = services.GetRequiredService(); - var result = await gitHubService.ApprovePullRequestAsync( - options.Owner!, - options.Repo!, - options.PrNumber, - options.Comment); - - Console.WriteLine($"PR approved: {result}"); - return 0; - } + return new ApproveCommandHandler(options, prAnalysisService, gitHubService); } - static async Task RunCommentCommandAsync(string[] args, IServiceProvider services) + private static ICommandHandler CreateCommentHandler(string[] args, IServiceProvider services) { - // commentコマンドのみの場合はヘルプを表示 - if (args.Length == 1) - { - Console.WriteLine("コメント投稿機能の使い方:"); - Console.WriteLine(); - Console.WriteLine("基本形式:"); - Console.WriteLine(" comment @123 コメント内容"); - Console.WriteLine(" comment src/file.cs@123 コメント内容"); - Console.WriteLine(); - Console.WriteLine("Suggestion付き:"); - Console.WriteLine(" comment @123 コメント内容 --suggestion \"修正コード\""); - Console.WriteLine(); - Console.WriteLine("承認付き:"); - Console.WriteLine(" comment @123 コメント内容 --approve"); - Console.WriteLine(); - Console.WriteLine("複数コメント:"); - Console.WriteLine(" comment @123 コメント1 @456 コメント2"); - Console.WriteLine(); - Console.WriteLine("行範囲指定:"); - Console.WriteLine(" comment @45-67 このセクション全体を見直してください"); - Console.WriteLine(); - Console.WriteLine("必須オプション:"); - Console.WriteLine(" --owner, -o リポジトリオーナー"); - Console.WriteLine(" --repo, -r リポジトリ名"); - Console.WriteLine(" --pr, -p プルリクエスト番号"); - Console.WriteLine(" --approve コメント投稿後にPRを承認"); - Console.WriteLine(); - Console.WriteLine("例:"); - Console.WriteLine(" PRAgent comment --owner myorg --repo myrepo --pr 123 @150 ここを修正してください"); - Console.WriteLine(" PRAgent comment -o myorg -r myrepo -p 123 @200 不適切なコード --suggestion \"適切なコード\""); - Console.WriteLine(" PRAgent comment --owner myorg --repo myrepo --pr 123 @100 \"修正が必要\" --approve"); - return 0; - } - var options = CommentCommandOptions.Parse(args); - - if (!options.IsValid(out var errors)) - { - Log.Error("Invalid options:"); - foreach (var error in errors) - { - Log.Error(" - {Error}", error); - } - return 1; - } - var gitHubService = services.GetRequiredService(); - - 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: c.LineNumber, - 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; - } - } - - static ReviewOptions ParseReviewOptions(string[] args) - { - var options = new ReviewOptions(); - - for (int i = 1; i < args.Length; i++) - { - switch (args[i]) - { - case "--owner": - case "-o": - if (i + 1 < args.Length) - options = options with { Owner = args[++i] }; - break; - case "--repo": - case "-r": - if (i + 1 < args.Length) - options = options with { Repo = args[++i] }; - break; - case "--pr": - case "-p": - case "--number": - if (i + 1 < args.Length && int.TryParse(args[++i], out var pr)) - options = options with { PrNumber = pr }; - break; - case "--post-comment": - case "-c": - options = options with { PostComment = true }; - break; - case "--auto-commit": - options = options with { AutoCommit = true }; - break; - } - } - - return options; - } - - static SummaryOptions ParseSummaryOptions(string[] args) - { - var options = new SummaryOptions(); - - for (int i = 1; i < args.Length; i++) - { - switch (args[i]) - { - case "--owner": - case "-o": - if (i + 1 < args.Length) - options = options with { Owner = args[++i] }; - break; - case "--repo": - case "-r": - if (i + 1 < args.Length) - options = options with { Repo = args[++i] }; - break; - case "--pr": - case "-p": - case "--number": - if (i + 1 < args.Length && int.TryParse(args[++i], out var pr)) - options = options with { PrNumber = pr }; - break; - case "--post-comment": - case "-c": - options = options with { PostComment = true }; - break; - case "--auto-commit": - options = options with { AutoCommit = true }; - break; - } - } - - return options; - } - - static ApproveOptions ParseApproveOptions(string[] args) - { - var options = new ApproveOptions(); - - for (int i = 1; i < args.Length; i++) - { - switch (args[i]) - { - case "--owner": - case "-o": - if (i + 1 < args.Length) - options = options with { Owner = args[++i] }; - break; - case "--repo": - case "-r": - if (i + 1 < args.Length) - options = options with { Repo = args[++i] }; - break; - case "--pr": - case "-p": - case "--number": - if (i + 1 < args.Length && int.TryParse(args[++i], out var pr)) - options = options with { PrNumber = pr }; - break; - case "--auto": - options = options with { Auto = true }; - break; - case "--threshold": - case "-t": - if (i + 1 < args.Length) - options = options with { Threshold = ParseThreshold(args[++i]) }; - break; - case "--comment": - case "-m": - if (i + 1 < args.Length) - options = options with { Comment = args[++i] }; - break; - case "--post-comment": - case "-c": - options = options with { PostComment = true }; - break; - case "--auto-commit": - options = options with { AutoCommit = true }; - break; - } - } - - return options; - } - - static ApprovalThreshold ParseThreshold(string value) - { - return value.ToLowerInvariant() switch - { - "critical" => ApprovalThreshold.Critical, - "major" => ApprovalThreshold.Major, - "minor" => ApprovalThreshold.Minor, - "none" => ApprovalThreshold.None, - _ => ApprovalThreshold.Minor - }; - } - - static void ShowHelp() - { - Console.WriteLine(""" - PRAgent - AI-powered Pull Request Agent - - USAGE: - PRAgent [options] - - COMMANDS: - review Review a pull request - summary Summarize a pull request - approve Approve a pull request - comment Post a comment on a specific line - help Show this help message - - REVIEW OPTIONS: - --owner, -o Repository owner (required) - --repo, -r Repository name (required) - --pr, -p Pull request number (required) - --post-comment, -c Post review as PR comment - - SUMMARY OPTIONS: - --owner, -o Repository owner (required) - --repo, -r Repository name (required) - --pr, -p Pull request number (required) - --post-comment, -c Post summary as PR comment - - APPROVE OPTIONS: - --owner, -o Repository owner (required) - --repo, -r Repository name (required) - --pr, -p Pull request number (required) - --auto Review first, then approve based on AI decision - --threshold, -t Approval threshold (critical|major|minor|none, default: minor) - --comment, -m Approval comment (only without --auto) - --post-comment, -c Post decision as PR comment (only with --auto) - - COMMENT OPTIONS: - --owner, -o Repository owner (required) - --repo, -r Repository name (required) - --pr, -p Pull request number (required) - --approve Approve the PR after posting comments - - COMMENT FORMAT: - comment @123 "This needs improvement" - comment @45-67 "Review this section" - comment src/file.cs@123 "Fix this" --suggestion "Fixed code" - comment @123 "Comment1" @456 "Comment2" - - EXAMPLES: - PRAgent review --owner "org" --repo "repo" --pr 123 - PRAgent review -o "org" -r "repo" -p 123 --post-comment - PRAgent summary --owner "org" --repo "repo" --pr 123 - PRAgent approve --owner "org" --repo "repo" --pr 123 --auto - PRAgent approve -o "org" -r "repo" -p 123 --auto --threshold major - PRAgent approve --owner "org" --repo "repo" --pr 123 --comment "LGTM" - PRAgent comment --owner "org" --repo "repo" --pr 123 @150 "This part is wrong" --suggestion "This part is correct" - PRAgent comment -o "org" -r "repo" -p 123 @100 "Fix this" --approve - """); - } - - record ReviewOptions - { - public string? Owner { get; init; } - public string? Repo { get; init; } - public int PrNumber { get; init; } - public bool PostComment { get; init; } - public bool AutoCommit { get; init; } - - public bool IsValid(out List errors) - { - errors = new List(); - - if (string.IsNullOrEmpty(Owner)) - errors.Add("--owner is required"); - if (string.IsNullOrEmpty(Repo)) - errors.Add("--repo is required"); - if (PrNumber <= 0) - errors.Add("--pr is required and must be a positive number"); - - return errors.Count == 0; - } - } - - record SummaryOptions - { - public string? Owner { get; init; } - public string? Repo { get; init; } - public int PrNumber { get; init; } - public bool PostComment { get; init; } - public bool AutoCommit { get; init; } - - public bool IsValid(out List errors) - { - errors = new List(); - - if (string.IsNullOrEmpty(Owner)) - errors.Add("--owner is required"); - if (string.IsNullOrEmpty(Repo)) - errors.Add("--repo is required"); - if (PrNumber <= 0) - errors.Add("--pr is required and must be a positive number"); - - return errors.Count == 0; - } - } - - record ApproveOptions - { - public string? Owner { get; init; } - public string? Repo { get; init; } - public int PrNumber { get; init; } - public bool Auto { get; init; } - public ApprovalThreshold Threshold { get; init; } = ApprovalThreshold.Minor; - public string? Comment { get; init; } - public bool PostComment { get; init; } - public bool AutoCommit { get; init; } - - public bool IsValid(out List errors) - { - errors = new List(); - - if (string.IsNullOrEmpty(Owner)) - errors.Add("--owner is required"); - if (string.IsNullOrEmpty(Repo)) - errors.Add("--repo is required"); - if (PrNumber <= 0) - errors.Add("--pr is required and must be a positive number"); - - return errors.Count == 0; - } + return new CommentCommandHandler(options, gitHubService); } -} \ No newline at end of file +} diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index 1d61d48..54918a1 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -95,6 +95,39 @@ public async Task ReviewAndApproveAsync( }; } + /// + /// プルリクエストのコードレビューを実行します(language指定) + /// + public async Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) + { + // languageパラメータは現在のSK実装では使用しない + return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + } + + /// + /// プルリクエストの要約を作成します(language指定) + /// + public async Task SummarizeAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) + { + // languageパラメータは現在のSK実装では使用しない + return await _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + } + + /// + /// レビューと承認を一連のワークフローとして実行します(language指定) + /// + public async Task ReviewAndApproveAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold, + string language, + CancellationToken cancellationToken = default) + { + // languageパラメータは現在のSK実装では使用しない + return await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken); + } + /// /// AgentGroupChatを使用したマルチエージェント協調によるレビューと承認 /// From b477ea7bf2cd8c0ffcbd097a945ef7612b46f51a Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:39:28 +0900 Subject: [PATCH 05/27] [fix] test --- .../YamlConfigurationProviderTests.cs | 136 ------------------ .../YamlConfigurationProvider.cs | 47 ------ 2 files changed, 183 deletions(-) delete mode 100644 PRAgent.Tests/YamlConfigurationProviderTests.cs delete mode 100644 PRAgent/Configuration/YamlConfigurationProvider.cs 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/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(yaml); - return true; - } - catch - { - return false; - } - } -} From b92ccfd2ab19ecb2bbed647182a016ecba7a528f Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:41:15 +0900 Subject: [PATCH 06/27] [add] github token --- PRAgent/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRAgent/Program.cs b/PRAgent/Program.cs index dda9e91..bf5e6d9 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -76,7 +76,7 @@ static async Task Main(string[] args) services.AddSingleton(_ => prAgentConfig); // GitHub Service - services.AddSingleton(); + services.AddSingleton(_ => new GitHubService(prSettings.GitHubToken)); // Kernel Service services.AddSingleton(); From 160f264adc392387d33017cb97161e2986c71143 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:42:51 +0900 Subject: [PATCH 07/27] [add] IDetailedCommentAgent --- PRAgent/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PRAgent/Program.cs b/PRAgent/Program.cs index bf5e6d9..3f9e3b2 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -88,6 +88,7 @@ static async Task Main(string[] args) services.AddSingleton(); // Agents (現在稼働中) + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From a513340cf6dca3e68732efd9938aeada6b16e4f5 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:44:26 +0900 Subject: [PATCH 08/27] [fix] di --- PRAgent/Services/AgentOrchestratorService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRAgent/Services/AgentOrchestratorService.cs b/PRAgent/Services/AgentOrchestratorService.cs index dc063e1..0544de4 100644 --- a/PRAgent/Services/AgentOrchestratorService.cs +++ b/PRAgent/Services/AgentOrchestratorService.cs @@ -9,14 +9,14 @@ public class AgentOrchestratorService : IAgentOrchestratorService private readonly ReviewAgent _reviewAgent; private readonly ApprovalAgent _approvalAgent; private readonly SummaryAgent _summaryAgent; - private readonly DetailedCommentAgent _detailedCommentAgent; + private readonly IDetailedCommentAgent _detailedCommentAgent; private readonly IGitHubService _gitHubService; public AgentOrchestratorService( ReviewAgent reviewAgent, ApprovalAgent approvalAgent, SummaryAgent summaryAgent, - DetailedCommentAgent detailedCommentAgent, + IDetailedCommentAgent detailedCommentAgent, IGitHubService gitHubService) { _reviewAgent = reviewAgent; From 193f3da579abd57f3ae535d710d80f1de6036308 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:21:28 +0900 Subject: [PATCH 09/27] [fix] agent mode --- .github/workflows/pr-review.yml | 1 + PRAgent/Agents/AgentFactory.cs | 19 +- PRAgent/Agents/ApprovalAgent.cs | 69 ------- PRAgent/Agents/BaseAgent.cs | 61 ------ PRAgent/Agents/ReviewAgent.cs | 175 ------------------ PRAgent/Agents/SK/SKApprovalAgent.cs | 79 +++++--- PRAgent/Agents/SummaryAgent.cs | 122 ------------ PRAgent/CommandLine/CommandLineOptions.cs | 2 +- .../ServiceCollectionExtensions.cs | 18 +- PRAgent/Models/PRActionBuffer.cs | 36 +++- PRAgent/Plugins/GitHub/ApprovePRFunction.cs | 122 ++++++------ PRAgent/Plugins/GitHub/PostCommentFunction.cs | 165 ++++------------- PRAgent/Plugins/PRActionFunctions.cs | 21 ++- PRAgent/Program.cs | 19 +- PRAgent/Services/AgentOrchestratorService.cs | 142 -------------- PRAgent/Services/ConfigurationService.cs | 4 +- PRAgent/Services/IAgentOrchestratorService.cs | 65 ++++++- PRAgent/Services/PRActionExecutor.cs | 53 ++++-- PRAgent/Services/PRAnalysisService.cs | 2 +- .../Services/SK/SKAgentOrchestratorService.cs | 82 +++----- PRAgent/appsettings.json | 4 +- 21 files changed, 366 insertions(+), 895 deletions(-) delete mode 100644 PRAgent/Agents/ApprovalAgent.cs delete mode 100644 PRAgent/Agents/BaseAgent.cs delete mode 100644 PRAgent/Agents/ReviewAgent.cs delete mode 100644 PRAgent/Agents/SummaryAgent.cs delete mode 100644 PRAgent/Services/AgentOrchestratorService.cs diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index ad17593..faff460 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -38,6 +38,7 @@ 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' }} diff --git a/PRAgent/Agents/AgentFactory.cs b/PRAgent/Agents/AgentFactory.cs index 08a060a..b3b57f2 100644 --- a/PRAgent/Agents/AgentFactory.cs +++ b/PRAgent/Agents/AgentFactory.cs @@ -96,7 +96,7 @@ public async Task CreateApprovalAgentAsync( string? customSystemPrompt = null, IEnumerable? functions = null) { - var kernel = _kernelService.CreateAgentKernel(AgentDefinition.ApprovalAgent.SystemPrompt); + var kernel = CreateApprovalKernel(owner, repo, prNumber, customSystemPrompt); // GitHub操作用のプラグインを登録 if (functions != null) @@ -119,13 +119,28 @@ public async Task CreateApprovalAgentAsync( ["approval_mode"] = true, ["owner"] = owner, ["repo"] = repo, - ["pr_number"] = prNumber + ["pr_number"] = prNumber, + // FunctionCallingを自動的に有効にする + ["function_choice_behavior"] = "auto" } }; return await Task.FromResult(agent); } + /// + /// Approvalエージェント用のKernelを作成 + /// + public Kernel CreateApprovalKernel( + string owner, + string repo, + int prNumber, + string? customSystemPrompt = null) + { + return _kernelService.CreateAgentKernel( + customSystemPrompt ?? AgentDefinition.ApprovalAgent.SystemPrompt); + } + /// /// カスタムエージェントを作成(汎用メソッド) /// 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/ReviewAgent.cs b/PRAgent/Agents/ReviewAgent.cs deleted file mode 100644 index 4a60152..0000000 --- a/PRAgent/Agents/ReviewAgent.cs +++ /dev/null @@ -1,175 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using PRAgent.Models; -using PRAgent.Services; -using PRAgent.Plugins; - -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; - } - - /// - /// Function Callingを使用してレビューを実行し、アクションをバッファに蓄積します - /// - public async Task ReviewWithActionsAsync( - string owner, - string repo, - int prNumber, - PRActionBuffer buffer, - CancellationToken cancellationToken = default) - { - var (pr, files, diff) = await GetPRDataAsync(owner, repo, prNumber); - var fileList = PullRequestDataService.FormatFileList(files); - - // Kernelを作成して関数を登録 - var kernel = CreateKernel(); - var actionFunctions = new PRActionFunctions(buffer); - kernel.ImportPluginFromObject(actionFunctions, "pr_actions"); - - 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. - - You have access to the following functions to accumulate your review actions: - - add_line_comment: Add a comment on a specific line of code - - add_review_comment: Add a general review comment - - add_summary: Add a summary of your review - - Instructions: - 1. Review the code changes thoroughly - 2. For each issue you find, use add_line_comment to mark the specific location - 3. If you have suggestions, include them in the line comment - 4. Use add_review_comment for general observations that don't apply to specific lines - 5. Use add_summary to provide an overall summary of your review - 6. When done, use ready_to_commit to indicate you're finished - - Only mark CRITICAL and MAJOR issues. Ignore MINOR issues unless there are many of them. - - File format for add_line_comment: use the exact file path as shown in the diff - """; - - var prompt = $""" - {systemPrompt} - - ## Pull Request Information - - Title: {pr.Title} - - Author: {pr.User.Login} - - Description: {pr.Body ?? "No description provided"} - - Branch: {pr.Head.Ref} -> {pr.Base.Ref} - - ## Changed Files - {fileList} - - ## Diff - {diff} - - Please review this pull request and use the available functions to add comments and mark issues. - When you're done, call ready_to_commit. - """; - - // 注: Semantic Kernel 1.68.0でのFunction Callingは、複雑なTool Call処理が必要です - // 現在は簡易的な実装として、通常のレビューを実行します - // 将来的には、Auto Commitオプションで完全なFunction Callingを実装予定 - - var resultBuilder = new System.Text.StringBuilder(); - - // 通常のレビューを実行して結果を取得 - var reviewResult = await KernelService.InvokePromptAsStringAsync(kernel, prompt, cancellationToken); - resultBuilder.Append(reviewResult); - - // 注: 実際のFunction Calling実装時は、ここでTool Callを処理してバッファに追加 - - // バッファの状態を追加(デモ用) - var state = buffer.GetState(); - resultBuilder.AppendLine($"\n\n## Review Summary"); - resultBuilder.AppendLine($"Line comments added: {state.LineCommentCount}"); - resultBuilder.AppendLine($"Review comments added: {state.ReviewCommentCount}"); - resultBuilder.AppendLine($"Summaries added: {state.SummaryCount}"); - - return resultBuilder.ToString(); - } -} diff --git a/PRAgent/Agents/SK/SKApprovalAgent.cs b/PRAgent/Agents/SK/SKApprovalAgent.cs index 2ec2f3c..6523bb7 100644 --- a/PRAgent/Agents/SK/SKApprovalAgent.cs +++ b/PRAgent/Agents/SK/SKApprovalAgent.cs @@ -123,38 +123,40 @@ public async Task ApproveAsync( } /// - /// GitHub操作関数を持つApprovalエージェントを作成します + /// バッファを使用してGitHub操作関数を持つApprovalエージェントを作成します /// - public async Task CreateAgentWithGitHubFunctionsAsync( + public async Task CreateAgentWithBufferAsync( string owner, string repo, int prNumber, + PRActionBuffer buffer, string? customSystemPrompt = null) { - var kernel = _agentFactory; + // プラグインインスタンスを作成 + var approvePlugin = new ApprovePRFunction(buffer); + var commentPlugin = new PostCommentFunction(buffer); - // GitHub操作用のプラグインを作成 - var approveFunction = new ApprovePRFunction(_gitHubService, owner, repo, prNumber); - var commentFunction = new PostCommentFunction(_gitHubService, owner, repo, prNumber); + // Kernelを作成してプラグインを登録 + var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, customSystemPrompt); + kernel.ImportPluginFromObject(approvePlugin); + kernel.ImportPluginFromObject(commentPlugin); - // 関数をKernelFunctionとして登録 - var functions = new List + // エージェントを作成 + var agent = new ChatCompletionAgent { - ApprovePRFunction.ApproveAsyncFunction(_gitHubService, owner, repo, prNumber), - ApprovePRFunction.GetApprovalStatusFunction(_gitHubService, owner, repo, prNumber), - PostCommentFunction.PostCommentAsyncFunction(_gitHubService, owner, repo, prNumber), - PostCommentFunction.PostReviewCommentAsyncFunction(_gitHubService, owner, repo, prNumber), - PostCommentFunction.PostLineCommentAsyncFunction(_gitHubService, owner, repo, prNumber) + Name = AgentDefinition.ApprovalAgent.Name, + Description = AgentDefinition.ApprovalAgent.Description, + Instructions = customSystemPrompt ?? AgentDefinition.ApprovalAgent.SystemPrompt, + Kernel = kernel }; - return await _agentFactory.CreateApprovalAgentAsync( - owner, repo, prNumber, customSystemPrompt, functions); + return await Task.FromResult(agent); } /// - /// 関数呼び出し機能を持つApprovalエージェントを使用して決定を行います + /// バッファリングパターンを使用して決定を行い、完了後にアクションを一括実行します /// - public async Task<(bool ShouldApprove, string Reasoning, string? Comment)> DecideWithFunctionCallingAsync( + public async Task<(bool ShouldApprove, string Reasoning, string? Comment, PRActionResult? ActionResult)> DecideWithFunctionCallingAsync( string owner, string repo, int prNumber, @@ -163,8 +165,11 @@ public async Task CreateAgentWithGitHubFunctionsAsync( bool autoApprove = false, CancellationToken cancellationToken = default) { - // GitHub関数を持つエージェントを作成 - var agent = await CreateAgentWithGitHubFunctionsAsync(owner, repo, prNumber); + // バッファを作成 + var buffer = new PRActionBuffer(); + + // バッファを使用したエージェントを作成 + var agent = await CreateAgentWithBufferAsync(owner, repo, prNumber, buffer); // PR情報を取得 var pr = await _gitHubService.GetPullRequestAsync(owner, repo, prNumber); @@ -172,8 +177,8 @@ public async Task CreateAgentWithGitHubFunctionsAsync( // プロンプトを作成 var autoApproveInstruction = autoApprove - ? "If the decision is to APPROVE, use the approve_pull_request function to actually approve the PR." - : ""; + ? "If the decision is to APPROVE, use the approve_pull_request function to add approval to buffer." + : "If the decision is to APPROVE, clearly state DECISION: APPROVE in your response."; var prompt = $""" Based on the code review below, make an approval decision for this pull request. @@ -190,30 +195,52 @@ public async Task CreateAgentWithGitHubFunctionsAsync( Your task: 1. Analyze the review results against the approval threshold - 2. Make a decision (APPROVE or REJECT) + 2. Make a decision (APPROVE, CHANGES_REQUESTED, or COMMENT_ONLY) 3. {autoApproveInstruction} - 4. If there are specific concerns that need to be addressed, use post_pr_comment or post_line_comment + 4. If the decision is CHANGES_REQUESTED, use the request_changes function to add changes requested to buffer. + 5. If there are specific concerns that need to be addressed, use: + - post_pr_comment for general comments + - post_line_comment for specific line-level feedback + - post_review_comment for review-level comments + + All actions will be buffered and executed after your analysis is complete. Provide your decision in this format: - DECISION: [APPROVE/REJECT] + DECISION: [APPROVE/CHANGES_REQUESTED/COMMENT_ONLY] 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. + Be conservative - when in doubt, request changes or add comments. """; 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); } + var responseText = responses.ToString(); + // レスポンスを解析 - return ApprovalResponseParser.Parse(responses.ToString()); + var (shouldApprove, reasoning, comment) = ApprovalResponseParser.Parse(responseText); + + // バッファの内容を実行 + PRActionResult? actionResult = null; + var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber); + var state = buffer.GetState(); + + if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || + state.HasGeneralComment || state.ApprovalState != PRApprovalState.None) + { + actionResult = await executor.ExecuteAsync(buffer, cancellationToken); + } + + return (shouldApprove, reasoning, comment, actionResult); } } diff --git a/PRAgent/Agents/SummaryAgent.cs b/PRAgent/Agents/SummaryAgent.cs deleted file mode 100644 index b5b09c9..0000000 --- a/PRAgent/Agents/SummaryAgent.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using PRAgent.Models; -using PRAgent.Services; -using PRAgent.Plugins; - -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); - } - - /// - /// Function Calling��g�p���ăT�}���[��쐬���A�A�N�V������o�b�t�@�ɒ~�ς��܂� - /// - public async Task SummarizeWithActionsAsync( - string owner, - string repo, - int prNumber, - PRActionBuffer buffer, - CancellationToken cancellationToken = default) - { - var (pr, files, diff) = await GetPRDataAsync(owner, repo, prNumber); - var fileList = PullRequestDataService.FormatFileList(files); - - // Kernel��쐬���Ċ֐���o�^ - var kernel = CreateKernel(); - var actionFunctions = new PRActionFunctions(buffer); - kernel.ImportPluginFromObject(actionFunctions, "pr_actions"); - - var systemPrompt = """ - You are a technical writer specializing in creating clear, concise documentation. - Your role is to summarize pull request changes accurately. - - You have access to the following functions: - - add_summary: Add a summary of the pull request - - set_general_comment: Set a general comment to post - - ready_to_commit: Indicate you're finished - - Instructions: - 1. Analyze the pull request changes - 2. Create a concise summary (under 300 words) using add_summary - 3. Optionally add additional context using set_general_comment - 4. When done, call ready_to_commit - - Focus on: - - Purpose: What does this PR achieve? - - Key Changes: Main files/components modified - - Impact: Areas affected - - Risk Assessment: Low/Medium/High with justification - - Testing Notes: Areas needing special attention - """; - - var prompt = $""" - {systemPrompt} - - ## Pull Request - - Title: {pr.Title} - - Author: {pr.User.Login} - - Description: {pr.Body ?? "No description provided"} - - Branch: {pr.Head.Ref} -> {pr.Base.Ref} - - ## Changed Files - {fileList} - - ## Diff - {diff} - - Please summarize this pull request and use the available functions to add the summary. - When you're done, call ready_to_commit. - """; - - // ��: Semantic Kernel 1.68.0�ł�Function Calling�́A���G��Tool Call�������K�v�ł� - // ���݂͊ȈՓI�Ȏ����Ƃ��āA�ʏ�̃T�}���[����s���܂� - // �����I�ɂ́AAuto Commit�I�v�V�����Ŋ��S��Function Calling������\�� - - var resultBuilder = new System.Text.StringBuilder(); - - // �ʏ�̃T�}���[����s���Č��ʂ�擾 - var summaryResult = await KernelService.InvokePromptAsStringAsync(kernel, prompt, cancellationToken); - resultBuilder.Append(summaryResult); - - // ��: ���ۂ�Function Calling�������́A������Tool Call��������ăo�b�t�@�ɒlj� - - // �o�b�t�@�̏�Ԃ�lj��i�f���p�j - var state = buffer.GetState(); - resultBuilder.AppendLine($"\n\n## Summary Summary"); - resultBuilder.AppendLine($"Summaries added: {state.SummaryCount}"); - resultBuilder.AppendLine($"General comment set: {state.HasGeneralComment}"); - - return resultBuilder.ToString(); - } -} 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/Configuration/ServiceCollectionExtensions.cs b/PRAgent/Configuration/ServiceCollectionExtensions.cs index e550e2d..f0eebc1 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,17 @@ 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(); + services.AddSingleton(); + services.AddSingleton(); + + // Agent Orchestrator - SKAgentOrchestratorServiceを使用 + services.AddSingleton(); // PR Analysis Service services.AddSingleton(); diff --git a/PRAgent/Models/PRActionBuffer.cs b/PRAgent/Models/PRActionBuffer.cs index 7002ae7..e87a9ed 100644 --- a/PRAgent/Models/PRActionBuffer.cs +++ b/PRAgent/Models/PRActionBuffer.cs @@ -10,7 +10,7 @@ public class PRActionBuffer private readonly List _reviewComments = new(); private readonly List _summaries = new(); private string? _generalComment; - private bool _shouldApprove; + private PRApprovalState _approvalState = PRApprovalState.None; private string? _approvalComment; /// @@ -55,11 +55,20 @@ public void SetGeneralComment(string comment) } /// - /// 承認をマークします + /// PRを承認します /// public void MarkForApproval(string? comment = null) { - _shouldApprove = true; + _approvalState = PRApprovalState.Approved; + _approvalComment = comment; + } + + /// + /// 変更を依頼します + /// + public void MarkForChangesRequested(string? comment = null) + { + _approvalState = PRApprovalState.ChangesRequested; _approvalComment = comment; } @@ -72,7 +81,7 @@ public void Clear() _reviewComments.Clear(); _summaries.Clear(); _generalComment = null; - _shouldApprove = false; + _approvalState = PRApprovalState.None; _approvalComment = null; } @@ -87,7 +96,7 @@ public PRActionState GetState() ReviewCommentCount = _reviewComments.Count, SummaryCount = _summaries.Count, HasGeneralComment = !string.IsNullOrEmpty(_generalComment), - ShouldApprove = _shouldApprove + ApprovalState = _approvalState }; } @@ -98,10 +107,23 @@ public PRActionState GetState() public IReadOnlyList ReviewComments => _reviewComments.AsReadOnly(); public IReadOnlyList Summaries => _summaries.AsReadOnly(); public string? GeneralComment => _generalComment; - public bool ShouldApprove => _shouldApprove; + public PRApprovalState ApprovalState => _approvalState; public string? ApprovalComment => _approvalComment; } +/// +/// PR承認ステータス +/// +public enum PRApprovalState +{ + /// なし(コメントのみ) + None, + /// 承認 + Approved, + /// 変更依頼 + ChangesRequested +} + /// /// 行コメントアクション /// @@ -130,5 +152,5 @@ public class PRActionState public int ReviewCommentCount { get; init; } public int SummaryCount { get; init; } public bool HasGeneralComment { get; init; } - public bool ShouldApprove { get; init; } + public PRApprovalState ApprovalState { get; init; } } diff --git a/PRAgent/Plugins/GitHub/ApprovePRFunction.cs b/PRAgent/Plugins/GitHub/ApprovePRFunction.cs index 11cfd55..abc95c0 100644 --- a/PRAgent/Plugins/GitHub/ApprovePRFunction.cs +++ b/PRAgent/Plugins/GitHub/ApprovePRFunction.cs @@ -1,86 +1,73 @@ using Microsoft.SemanticKernel; -using PRAgent.Services; +using PRAgent.Models; namespace PRAgent.Plugins.GitHub; /// -/// Semantic Kernel用のプルリクエスト承認機能プラグイン +/// Semantic Kernel用のプルリクエスト承認機能プラグイン(バッファリング版) +/// Approve/Changes Request/Comment Onlyを区別 /// public class ApprovePRFunction { - private readonly IGitHubService _gitHubService; - private readonly string _owner; - private readonly string _repo; - private readonly int _prNumber; + private readonly PRActionBuffer _buffer; - public ApprovePRFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public ApprovePRFunction(PRActionBuffer buffer) { - _gitHubService = gitHubService; - _owner = owner; - _repo = repo; - _prNumber = prNumber; + _buffer = buffer; } /// - /// プルリクエストを承認します + /// プルリクエストを承認します(バッファに追加) /// /// 承認時に追加するコメント(オプション) - /// キャンセレーショントークン - /// 承認結果のメッセージ + /// 承認アクションが追加されたことを示すメッセージ [KernelFunction("approve_pull_request")] - public async Task ApproveAsync( - string? comment = null, - CancellationToken cancellationToken = default) + public string ApproveAsync(string? comment = null) { - try - { - var result = await _gitHubService.ApprovePullRequestAsync(_owner, _repo, _prNumber, comment); - return $"Pull request #{_prNumber} has been approved successfully.{(!string.IsNullOrEmpty(comment) ? $" Comment: {comment}" : "")}"; - } - catch (Exception ex) - { - return $"Failed to approve pull request #{_prNumber}: {ex.Message}"; - } + _buffer.MarkForApproval(comment); + return $"Pull request approval action has been added to buffer.{(!string.IsNullOrEmpty(comment) ? $" Comment: {comment}" : "")}"; } /// - /// プルリクエストの承認ステータスを確認します + /// プルリクエストの変更を依頼します(バッファに追加) /// - /// キャンセレーショントークン - /// 現在の承認ステータス + /// 変更依頼時のコメント(オプション) + /// 変更依頼アクションが追加されたことを示すメッセージ + [KernelFunction("request_changes")] + public string RequestChangesAsync(string? comment = null) + { + _buffer.MarkForChangesRequested(comment); + return $"Pull request changes requested action has been added to buffer.{(!string.IsNullOrEmpty(comment) ? $" Comment: {comment}" : "")}"; + } + + /// + /// プルリクエストの承認ステータスを確認します(ダミー実装) + /// + /// 現在のバッファ状態を含むステータス [KernelFunction("get_approval_status")] - public async Task GetApprovalStatusAsync( - CancellationToken cancellationToken = default) + public string GetApprovalStatus() { - try - { - var pr = await _gitHubService.GetPullRequestAsync(_owner, _repo, _prNumber); - return $"Pull request #{_prNumber} status: {pr.State}, mergeable: {pr.Mergeable ?? true}"; - } - catch (Exception ex) + var state = _buffer.GetState(); + var statusText = state.ApprovalState switch { - return $"Failed to get approval status for PR #{_prNumber}: {ex.Message}"; - } + PRApprovalState.None => "No approval action (comments only)", + PRApprovalState.Approved => "Approved", + PRApprovalState.ChangesRequested => "Changes Requested", + _ => "Unknown" + }; + return $"Current buffer state - ApprovalState: {statusText}, Comments: {state.ReviewCommentCount}"; } /// /// KernelFunctionとして使用するためのファクトリメソッド /// - public static KernelFunction ApproveAsyncFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public static KernelFunction ApproveAsyncFunction(PRActionBuffer buffer) { - var functionPlugin = new ApprovePRFunction(gitHubService, owner, repo, prNumber); + var functionPlugin = new ApprovePRFunction(buffer); return KernelFunctionFactory.CreateFromMethod( - (string? comment, CancellationToken ct) => functionPlugin.ApproveAsync(comment, ct), + (string? comment) => functionPlugin.ApproveAsync(comment), functionName: "approve_pull_request", - description: "Approves a pull request with an optional comment", + description: "Adds a pull request approval action to the buffer with an optional comment", parameters: new[] { new KernelParameterMetadata("comment") @@ -92,20 +79,37 @@ public static KernelFunction ApproveAsyncFunction( }); } + /// + /// KernelFunctionとして使用するためのファクトリメソッド(変更依頼) + /// + public static KernelFunction RequestChangesFunction(PRActionBuffer buffer) + { + var functionPlugin = new ApprovePRFunction(buffer); + return KernelFunctionFactory.CreateFromMethod( + (string? comment) => functionPlugin.RequestChangesAsync(comment), + functionName: "request_changes", + description: "Adds a pull request changes requested action to the buffer with an optional comment", + parameters: new[] + { + new KernelParameterMetadata("comment") + { + Description = "Optional comment explaining the changes requested", + DefaultValue = null, + IsRequired = false + } + }); + } + /// /// KernelFunctionとして使用するためのファクトリメソッド(ステータス取得) /// - public static KernelFunction GetApprovalStatusFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public static KernelFunction GetApprovalStatusFunction(PRActionBuffer buffer) { - var functionPlugin = new ApprovePRFunction(gitHubService, owner, repo, prNumber); + var functionPlugin = new ApprovePRFunction(buffer); return KernelFunctionFactory.CreateFromMethod( - (CancellationToken ct) => functionPlugin.GetApprovalStatusAsync(ct), + () => functionPlugin.GetApprovalStatus(), functionName: "get_approval_status", - description: "Gets the current approval status of a pull request" + description: "Gets the current buffer state including pending approval status" ); } } diff --git a/PRAgent/Plugins/GitHub/PostCommentFunction.cs b/PRAgent/Plugins/GitHub/PostCommentFunction.cs index 2aec491..6144de7 100644 --- a/PRAgent/Plugins/GitHub/PostCommentFunction.cs +++ b/PRAgent/Plugins/GitHub/PostCommentFunction.cs @@ -1,74 +1,51 @@ using Microsoft.SemanticKernel; -using Octokit; -using PRAgent.Services; +using PRAgent.Models; namespace PRAgent.Plugins.GitHub; /// -/// Semantic Kernel用のプルリクエストコメント投稿機能プラグイン +/// Semantic Kernel用のプルリクエストコメント投稿機能プラグイン(バッファリング版) /// public class PostCommentFunction { - private readonly IGitHubService _gitHubService; - private readonly string _owner; - private readonly string _repo; - private readonly int _prNumber; + private readonly PRActionBuffer _buffer; - public PostCommentFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public PostCommentFunction(PRActionBuffer buffer) { - _gitHubService = gitHubService; - _owner = owner; - _repo = repo; - _prNumber = prNumber; + _buffer = buffer; } /// - /// プルリクエストに全体コメントを投稿します + /// プルリクエストに全体コメントを追加します(バッファに追加) /// /// コメント内容 - /// キャンセレーショントークン - /// 投稿結果のメッセージ + /// コメントがバッファに追加されたことを示すメッセージ [KernelFunction("post_pr_comment")] - public async Task PostCommentAsync( - string comment, - CancellationToken cancellationToken = default) + public string PostCommentAsync(string comment) { if (string.IsNullOrWhiteSpace(comment)) { return "Error: Comment cannot be empty"; } - try - { - var result = await _gitHubService.CreateIssueCommentAsync(_owner, _repo, _prNumber, comment); - return $"Comment posted successfully to PR #{_prNumber}. Comment ID: {result.Id}"; - } - catch (Exception ex) - { - return $"Failed to post comment to PR #{_prNumber}: {ex.Message}"; - } + _buffer.SetGeneralComment(comment); + return $"General comment has been added to buffer. Length: {comment.Length} characters"; } /// - /// プルリクエストの特定の行にコメントを投稿します + /// プルリクエストの特定の行にコメントを追加します(バッファに追加) /// /// ファイルパス /// 行番号 /// コメント内容 /// 提案される変更内容(オプション) - /// キャンセレーショントークン - /// 投稿結果のメッセージ + /// 行コメントがバッファに追加されたことを示すメッセージ [KernelFunction("post_line_comment")] - public async Task PostLineCommentAsync( + public string PostLineCommentAsync( string filePath, int lineNumber, string comment, - string? suggestion = null, - CancellationToken cancellationToken = default) + string? suggestion = null) { if (string.IsNullOrWhiteSpace(filePath)) { @@ -80,104 +57,42 @@ public async Task PostLineCommentAsync( return "Error: Comment cannot be empty"; } - try - { - var result = await _gitHubService.CreateLineCommentAsync( - _owner, _repo, _prNumber, filePath, lineNumber, comment, suggestion); - return $"Line comment posted successfully to {filePath}:{lineNumber} in PR #{_prNumber}"; - } - catch (Exception ex) - { - return $"Failed to post line comment to PR #{_prNumber}: {ex.Message}"; - } + _buffer.AddLineComment(filePath, lineNumber, comment, suggestion); + return $"Line comment has been added to buffer for {filePath}:{lineNumber}"; } /// - /// 複数の行コメントを一度に投稿します - /// - /// コメントリスト(ファイルパス、行番号、コメント、提案) - /// キャンセレーショントークン - /// 投稿結果のメッセージ - [KernelFunction("post_multiple_line_comments")] - public async Task PostMultipleLineCommentsAsync( - string commentsJson, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(commentsJson)) - { - return "Error: Comments data cannot be empty"; - } - - try - { - // JSON形式のコメントデータをパース - var comments = System.Text.Json.JsonSerializer.Deserialize>( - commentsJson, - new System.Text.Json.JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - if (comments == null || comments.Count == 0) - { - return "Error: No valid comments found in the provided data"; - } - - var result = await _gitHubService.CreateMultipleLineCommentsAsync(_owner, _repo, _prNumber, comments); - return $"Successfully posted {comments.Count} line comments to PR #{_prNumber}"; - } - catch (Exception ex) - { - return $"Failed to post multiple line comments to PR #{_prNumber}: {ex.Message}"; - } - } - - /// - /// レビューコメントとして投稿します + /// レビューコメントを追加します(バッファに追加) /// /// レビュー本文 - /// キャンセレーショントークン - /// 投稿結果のメッセージ + /// レビューコメントがバッファに追加されたことを示すメッセージ [KernelFunction("post_review_comment")] - public async Task PostReviewCommentAsync( - string reviewBody, - CancellationToken cancellationToken = default) + public string PostReviewCommentAsync(string reviewBody) { if (string.IsNullOrWhiteSpace(reviewBody)) { return "Error: Review body cannot be empty"; } - try - { - var result = await _gitHubService.CreateReviewCommentAsync(_owner, _repo, _prNumber, reviewBody); - return $"Review comment posted successfully to PR #{_prNumber}. Review ID: {result.Id}"; - } - catch (Exception ex) - { - return $"Failed to post review comment to PR #{_prNumber}: {ex.Message}"; - } + _buffer.AddReviewComment(reviewBody); + return $"Review comment has been added to buffer. Length: {reviewBody.Length} characters"; } /// /// KernelFunctionとして使用するためのファクトリメソッド(PRコメント) /// - public static KernelFunction PostCommentAsyncFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public static KernelFunction PostCommentAsyncFunction(PRActionBuffer buffer) { - var functionPlugin = new PostCommentFunction(gitHubService, owner, repo, prNumber); + var functionPlugin = new PostCommentFunction(buffer); return KernelFunctionFactory.CreateFromMethod( - (string comment, CancellationToken ct) => functionPlugin.PostCommentAsync(comment, ct), + (string comment) => functionPlugin.PostCommentAsync(comment), functionName: "post_pr_comment", - description: "Posts a general comment to a pull request", + description: "Adds a general comment to the buffer for posting to a pull request", parameters: new[] { new KernelParameterMetadata("comment") { - Description = "The comment content to post", + Description = "The comment content to add to buffer", IsRequired = true } }); @@ -186,18 +101,14 @@ public static KernelFunction PostCommentAsyncFunction( /// /// KernelFunctionとして使用するためのファクトリメソッド(行コメント) /// - public static KernelFunction PostLineCommentAsyncFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public static KernelFunction PostLineCommentAsyncFunction(PRActionBuffer buffer) { - var functionPlugin = new PostCommentFunction(gitHubService, owner, repo, prNumber); + var functionPlugin = new PostCommentFunction(buffer); return KernelFunctionFactory.CreateFromMethod( - (string filePath, int lineNumber, string comment, string? suggestion, CancellationToken ct) => - functionPlugin.PostLineCommentAsync(filePath, lineNumber, comment, suggestion, ct), + (string filePath, int lineNumber, string comment, string? suggestion) => + functionPlugin.PostLineCommentAsync(filePath, lineNumber, comment, suggestion), functionName: "post_line_comment", - description: "Posts a comment on a specific line in a pull request file", + description: "Adds a line comment to the buffer for posting to a specific line in a pull request file", parameters: new[] { new KernelParameterMetadata("filePath") @@ -227,22 +138,18 @@ public static KernelFunction PostLineCommentAsyncFunction( /// /// KernelFunctionとして使用するためのファクトリメソッド(レビューコメント) /// - public static KernelFunction PostReviewCommentAsyncFunction( - IGitHubService gitHubService, - string owner, - string repo, - int prNumber) + public static KernelFunction PostReviewCommentAsyncFunction(PRActionBuffer buffer) { - var functionPlugin = new PostCommentFunction(gitHubService, owner, repo, prNumber); + var functionPlugin = new PostCommentFunction(buffer); return KernelFunctionFactory.CreateFromMethod( - (string reviewBody, CancellationToken ct) => functionPlugin.PostReviewCommentAsync(reviewBody, ct), + (string reviewBody) => functionPlugin.PostReviewCommentAsync(reviewBody), functionName: "post_review_comment", - description: "Posts a review comment to a pull request", + description: "Adds a review comment to the buffer for posting to a pull request", parameters: new[] { new KernelParameterMetadata("reviewBody") { - Description = "The review content to post", + Description = "The review content to add to buffer", IsRequired = true } }); diff --git a/PRAgent/Plugins/PRActionFunctions.cs b/PRAgent/Plugins/PRActionFunctions.cs index f2327dd..8f09228 100644 --- a/PRAgent/Plugins/PRActionFunctions.cs +++ b/PRAgent/Plugins/PRActionFunctions.cs @@ -79,13 +79,20 @@ public string MarkForApproval(string? comment = null) public string GetBufferState() { var state = _buffer.GetState(); + var approvalText = state.ApprovalState switch + { + PRApprovalState.Approved => "承認", + PRApprovalState.ChangesRequested => "変更依頼", + PRApprovalState.None => "なし", + _ => "なし" + }; return $""" 現在のバッファ状態: - 行コメント: {state.LineCommentCount}件 - レビューコメント: {state.ReviewCommentCount}件 - サマリー: {state.SummaryCount}件 - 全体コメント: {(state.HasGeneralComment ? "あり" : "なし")} - - 承認フラグ: {(state.ShouldApprove ? "オン" : "オフ")} + - 承認ステータス: {approvalText} """; } @@ -109,20 +116,28 @@ public string ReadyToCommit() var state = _buffer.GetState(); var totalActions = state.LineCommentCount + state.ReviewCommentCount + state.SummaryCount + (state.HasGeneralComment ? 1 : 0) + - (state.ShouldApprove ? 1 : 0); + (state.ApprovalState != PRApprovalState.None ? 1 : 0); if (totalActions == 0) { return "コミットするアクションがありません。"; } + var approvalText = state.ApprovalState switch + { + PRApprovalState.Approved => "承認", + PRApprovalState.ChangesRequested => "変更依頼", + PRApprovalState.None => "なし", + _ => "なし" + }; + return $""" {totalActions}件のアクションをコミット準備完了: - 行コメント: {state.LineCommentCount}件 - レビューコメント: {state.ReviewCommentCount}件 - サマリー: {state.SummaryCount}件 - 全体コメント: {(state.HasGeneralComment ? "あり" : "なし")} - - 承認: {(state.ShouldApprove ? "あり" : "なし")} + - 承認ステータス: {approvalText} これらのアクションをGitHubに投稿します。 """; diff --git a/PRAgent/Program.cs b/PRAgent/Program.cs index 3f9e3b2..4accf36 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -87,11 +87,8 @@ static async Task Main(string[] args) // Data Services services.AddSingleton(); - // Agents (現在稼働中) + // Detailed Comment Agent services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); // SK Agents (Semantic Kernel Agent Framework) services.AddSingleton(); @@ -99,17 +96,9 @@ static async Task Main(string[] args) services.AddSingleton(); services.AddSingleton(); - // Agent Orchestrator - AgentFramework設定に応じて切り替え - if (prAgentConfig.AgentFramework?.Enabled == true) - { - services.AddSingleton(); - Log.Information("Using SKAgentOrchestratorService (Agent Framework enabled)"); - } - else - { - services.AddSingleton(); - Log.Information("Using AgentOrchestratorService (standard mode)"); - } + // Agent Orchestrator - SKAgentOrchestratorServiceを使用 + services.AddSingleton(); + Log.Information("Using SKAgentOrchestratorService (Agent Framework)"); // PR Analysis Service services.AddSingleton(); diff --git a/PRAgent/Services/AgentOrchestratorService.cs b/PRAgent/Services/AgentOrchestratorService.cs deleted file mode 100644 index 0544de4..0000000 --- a/PRAgent/Services/AgentOrchestratorService.cs +++ /dev/null @@ -1,142 +0,0 @@ -using PRAgent.Agents; -using PRAgent.Models; -using PRAgent.Services; - -namespace PRAgent.Services; - -public class AgentOrchestratorService : IAgentOrchestratorService -{ - private readonly ReviewAgent _reviewAgent; - private readonly ApprovalAgent _approvalAgent; - private readonly SummaryAgent _summaryAgent; - private readonly IDetailedCommentAgent _detailedCommentAgent; - private readonly IGitHubService _gitHubService; - - public AgentOrchestratorService( - ReviewAgent reviewAgent, - ApprovalAgent approvalAgent, - SummaryAgent summaryAgent, - IDetailedCommentAgent detailedCommentAgent, - IGitHubService gitHubService) - { - _reviewAgent = reviewAgent; - _approvalAgent = approvalAgent; - _summaryAgent = summaryAgent; - _detailedCommentAgent = detailedCommentAgent; - _gitHubService = gitHubService; - } - - public async Task ReviewAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default) - { - return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken); - } - - public async Task SummarizeAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default) - { - return await _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken); - } - - public async Task ReviewAndApproveAsync( - string owner, - string repo, - int prNumber, - ApprovalThreshold threshold, - CancellationToken cancellationToken = default) - { - // Step 1: ReviewAgent performs the review - var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken); - - // Step 2: ApprovalAgent decides based on the review - var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( - owner, repo, prNumber, review, threshold, cancellationToken); - - // Step 3: If approved, execute the approval - string? approvalUrl = null; - if (shouldApprove) - { - var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); - approvalUrl = result; - } - - return new ApprovalResult - { - Approved = shouldApprove, - Review = review, - Reasoning = reasoning, - Comment = comment, - ApprovalUrl = approvalUrl - }; - } - - public async Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) - { - _reviewAgent.SetLanguage(language); - - var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken); - var detailedComments = await _detailedCommentAgent.CreateCommentsAsync(review, language); - - // レビューを投稿 - await _gitHubService.CreateReviewCommentAsync(owner, repo, prNumber, review); - - // 詳細コメントは個別に投稿(複数の場合はリクエスト制限に注意) - if (detailedComments.Count > 0) - { - // 最初のコメントを投稿(GitHub APIでは一度に複数コメントを投稿できる) - await _gitHubService.CreateReviewCommentAsync(owner, repo, prNumber, review); - } - - return review; - } - - public async Task SummarizeAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) - { - _summaryAgent.SetLanguage(language); - return await _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken); - } - - public async Task ReviewAndApproveAsync( - string owner, - string repo, - int prNumber, - ApprovalThreshold threshold, - string language, - CancellationToken cancellationToken = default) - { - _reviewAgent.SetLanguage(language); - _approvalAgent.SetLanguage(language); - - // Step 1: ReviewAgent performs the review - var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken); - var detailedComments = await _detailedCommentAgent.CreateCommentsAsync(review, language); - - // レビューを投稿 - await _gitHubService.CreateReviewCommentAsync(owner, repo, prNumber, review); - - // 詳細コメントは個別に投稿(複数の場合はリクエスト制限に注意) - if (detailedComments.Count > 0) - { - await _gitHubService.CreateReviewCommentAsync(owner, repo, prNumber, review); - } - - // Step 2: ApprovalAgent decides based on the review - var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( - owner, repo, prNumber, review, threshold, cancellationToken); - - // Step 3: If approved, execute the approval - string? approvalUrl = null; - if (shouldApprove) - { - var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); - approvalUrl = result; - } - - return new ApprovalResult - { - Approved = shouldApprove, - Review = review, - Reasoning = reasoning, - Comment = comment, - ApprovalUrl = approvalUrl - }; - } -} diff --git a/PRAgent/Services/ConfigurationService.cs b/PRAgent/Services/ConfigurationService.cs index 9caea2c..fb8a064 100644 --- a/PRAgent/Services/ConfigurationService.cs +++ b/PRAgent/Services/ConfigurationService.cs @@ -45,11 +45,11 @@ public Task GetConfigurationAsync(string owner, string repo, int IgnorePaths = _configuration.GetSection("PRAgent:IgnorePaths").Get>() ?? GetDefaultIgnorePaths(), AgentFramework = new AgentFrameworkConfig { - Enabled = _configuration.GetValue("PRAgent:AgentFramework:Enabled", false), + Enabled = _configuration.GetValue("PRAgent:AgentFramework:Enabled", true), OrchestrationMode = _configuration.GetValue("PRAgent:AgentFramework:OrchestrationMode", "sequential") ?? "sequential", SelectionStrategy = _configuration.GetValue("PRAgent:AgentFramework:SelectionStrategy", "approval_workflow") ?? "approval_workflow", EnableFunctionCalling = _configuration.GetValue("PRAgent:AgentFramework:EnableFunctionCalling", true), - EnableAutoApproval = _configuration.GetValue("PRAgent:AgentFramework:EnableAutoApproval", false), + EnableAutoApproval = _configuration.GetValue("PRAgent:AgentFramework:EnableAutoApproval", true), MaxTurns = _configuration.GetValue("PRAgent:AgentFramework:MaxTurns", 10) } }; diff --git a/PRAgent/Services/IAgentOrchestratorService.cs b/PRAgent/Services/IAgentOrchestratorService.cs index 35aeb57..3851bcb 100644 --- a/PRAgent/Services/IAgentOrchestratorService.cs +++ b/PRAgent/Services/IAgentOrchestratorService.cs @@ -1,34 +1,83 @@ -using PRAgent.Agents; using PRAgent.Models; namespace PRAgent.Services; +/// +/// エージェントオーケストレーションサービスのインターフェース +/// public interface IAgentOrchestratorService { + /// + /// プルリクエストのコードレビューを実行します + /// Task ReviewAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default); + + /// + /// プルリクエストの要約を作成します + /// Task SummarizeAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default); + + /// + /// レビューと承認を一連のワークフローとして実行します + /// Task ReviewAndApproveAsync( string owner, string repo, int prNumber, - ApprovalThreshold threshold, + ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default); + + /// + /// プルリクエストのコードレビューを実行します(language指定) + /// Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default); + + /// + /// プルリクエストの要約を作成します(language指定) + /// Task SummarizeAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default); + + /// + /// レビューと承認を一連のワークフローとして実行します(language指定) + /// Task ReviewAndApproveAsync( string owner, string repo, int prNumber, - ApprovalThreshold threshold, string language, + ApprovalThreshold threshold = ApprovalThreshold.Minor, + CancellationToken cancellationToken = default); + + /// + /// AgentGroupChatを使用したマルチエージェント協調によるレビューと承認 + /// + Task ReviewAndApproveWithAgentChatAsync( + string owner, + string repo, + int prNumber, + ApprovalThreshold threshold = ApprovalThreshold.Minor, + CancellationToken cancellationToken = default); + + /// + /// カスタムワークフローを使用したレビューと承認 + /// + Task ReviewAndApproveWithCustomWorkflowAsync( + string owner, + string repo, + int prNumber, + string workflowType, + ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default); } +/// +/// 承認結果 +/// public class ApprovalResult { - public bool Approved { get; set; } - public string Review { get; set; } = string.Empty; - public string Reasoning { get; set; } = string.Empty; - public string? Comment { get; set; } - public string? ApprovalUrl { get; set; } + public bool Approved { get; init; } + public string Review { get; init; } = string.Empty; + public string Reasoning { get; init; } = string.Empty; + public string? Comment { get; init; } + public string? ApprovalUrl { get; init; } } diff --git a/PRAgent/Services/PRActionExecutor.cs b/PRAgent/Services/PRActionExecutor.cs index 5d2bf0a..8f5e0c3 100644 --- a/PRAgent/Services/PRActionExecutor.cs +++ b/PRAgent/Services/PRActionExecutor.cs @@ -81,21 +81,39 @@ public async Task ExecuteAsync(PRActionBuffer buffer, Cancellati result.GeneralCommentUrl = commentResult.HtmlUrl; } - // 4. 承認を実行 - if (buffer.ShouldApprove) + // 4. 承認ステータスに応じた処理を実行 + switch (buffer.ApprovalState) { - var approvalResult = await _gitHubService.ApprovePullRequestAsync( - _owner, _repo, _prNumber, buffer.ApprovalComment); - - result.Approved = true; - result.ApprovalUrl = approvalResult.HtmlUrl; + case PRApprovalState.Approved: + var approvalResult = await _gitHubService.ApprovePullRequestAsync( + _owner, _repo, _prNumber, buffer.ApprovalComment); + + result.Approved = true; + result.ApprovalState = PRApprovalState.Approved; + result.ApprovalUrl = approvalResult.HtmlUrl; + break; + + case PRApprovalState.ChangesRequested: + // 変更依頼をレビューコメントとして投稿 + var changesComment = $"## Changes Requested\n\n{buffer.ApprovalComment ?? "Please address the issues mentioned in the review."}"; + await _gitHubService.CreateReviewCommentAsync( + _owner, _repo, _prNumber, changesComment); + + result.ApprovalState = PRApprovalState.ChangesRequested; + result.ChangesRequested = true; + break; + + case PRApprovalState.None: + // 何もしない(コメントのみ) + break; } result.TotalActionsPosted = result.LineCommentsPosted + result.SummariesPosted + (result.GeneralCommentPosted ? 1 : 0) + - (result.Approved ? 1 : 0); + (result.Approved ? 1 : 0) + + (result.ChangesRequested ? 1 : 0); result.Message = $"Successfully posted {result.TotalActionsPosted} action(s) to PR #{_prNumber}"; } @@ -147,15 +165,26 @@ public string CreatePreview(PRActionBuffer buffer) preview += $"### 全体コメント\n{buffer.GeneralComment.Substring(0, Math.Min(200, buffer.GeneralComment.Length))}...\n\n"; } - if (buffer.ShouldApprove) + // 承認ステータスに応じた表示 + switch (buffer.ApprovalState) { - preview += $"### 承認\nはい - {buffer.ApprovalComment ?? "コメントなし"}\n\n"; + case PRApprovalState.Approved: + preview += $"### 承認\nはい - {buffer.ApprovalComment ?? "コメントなし"}\n\n"; + break; + + case PRApprovalState.ChangesRequested: + preview += $"### 変更依頼\nはい - {buffer.ApprovalComment ?? "コメントなし"}\n\n"; + break; + + case PRApprovalState.None: + // 何も表示しない(コメントのみ) + break; } var totalActions = buffer.LineComments.Count + buffer.Summaries.Count + (string.IsNullOrEmpty(buffer.GeneralComment) ? 0 : 1) + - (buffer.ShouldApprove ? 1 : 0); + (buffer.ApprovalState != PRApprovalState.None ? 1 : 0); preview += $"**合計: {totalActions}件のアクション**"; @@ -177,6 +206,8 @@ public class PRActionResult public int SummariesPosted { get; set; } public bool GeneralCommentPosted { get; set; } public bool Approved { get; set; } + public bool ChangesRequested { get; set; } + public PRApprovalState? ApprovalState { get; set; } public string? SummaryCommentUrl { get; set; } public string? GeneralCommentUrl { get; set; } public string? ApprovalUrl { get; set; } diff --git a/PRAgent/Services/PRAnalysisService.cs b/PRAgent/Services/PRAnalysisService.cs index ddb3827..cc7d32f 100644 --- a/PRAgent/Services/PRAnalysisService.cs +++ b/PRAgent/Services/PRAnalysisService.cs @@ -115,7 +115,7 @@ public async Task ReviewAndApproveAsync( } var result = !string.IsNullOrEmpty(language) - ? await _agentOrchestrator.ReviewAndApproveAsync(owner, repo, prNumber, threshold, language) + ? await _agentOrchestrator.ReviewAndApproveAsync(owner, repo, prNumber, language, threshold) : await _agentOrchestrator.ReviewAndApproveAsync(owner, repo, prNumber, threshold); if (postComment && !result.Approved) diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index 54918a1..65bd350 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -75,14 +75,36 @@ public async Task ReviewAndApproveAsync( // ワークフロー: ReviewAgent → ApprovalAgent var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( - owner, repo, prNumber, review, threshold, cancellationToken); + // FunctionCalling設定に応じてメソッドを選択 + var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + var autoApprove = _config.AgentFramework?.EnableAutoApproval ?? false; + bool shouldApprove; + string reasoning; + string? comment; string? approvalUrl = null; - if (shouldApprove) + + if (useFunctionCalling) { - var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); - approvalUrl = result; + // FunctionCalling使用 - PRActionResultが返される + var (_, reasoningFc, commentFc, actionResult) = await _approvalAgent.DecideWithFunctionCallingAsync( + owner, repo, prNumber, review, threshold, autoApprove, cancellationToken); + shouldApprove = actionResult?.Approved ?? false; + reasoning = reasoningFc; + comment = commentFc; + approvalUrl = actionResult?.ApprovalUrl; + } + else + { + // FunctionCalling不使用 + (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( + owner, repo, prNumber, review, threshold, cancellationToken); + + if (shouldApprove) + { + var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); + approvalUrl = result; + } } return new ApprovalResult @@ -120,8 +142,8 @@ public async Task ReviewAndApproveAsync( string owner, string repo, int prNumber, - ApprovalThreshold threshold, string language, + ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default) { // languageパラメータは現在のSK実装では使用しない @@ -171,8 +193,8 @@ public async Task ReviewAndApproveWithCustomWorkflowAsync( string owner, string repo, int prNumber, - ApprovalThreshold threshold, string workflow, + ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default) { return workflow.ToLower() switch @@ -296,50 +318,4 @@ private async Task ParallelReviewWorkflowAsync( ApprovalUrl = approvalUrl }; } - - /// - /// 関数呼び出し機能を持つApprovalエージェントを作成 - /// - private async Task CreateApprovalAgentWithFunctionsAsync( - string owner, string repo, int prNumber) - { - // 設定を反映したカスタムプロンプトを作成 - var customPrompt = _config.AgentFramework?.Agents?.Approval?.CustomSystemPrompt; - - // GitHub操作用のプラグインを作成 - var approveFunction = new ApprovePRFunction(_gitHubService, owner, repo, prNumber); - var commentFunction = new PostCommentFunction(_gitHubService, owner, repo, prNumber); - - // 関数をKernelFunctionとして登録 - var functions = new List - { - ApprovePRFunction.ApproveAsyncFunction(_gitHubService, owner, repo, prNumber), - ApprovePRFunction.GetApprovalStatusFunction(_gitHubService, owner, repo, prNumber), - PostCommentFunction.PostCommentAsyncFunction(_gitHubService, owner, repo, prNumber), - PostCommentFunction.PostLineCommentAsyncFunction(_gitHubService, owner, repo, prNumber) - }; - - return await _agentFactory.CreateApprovalAgentAsync( - owner, repo, prNumber, customPrompt, functions); - } - - /// - /// 設定を反映したReviewエージェントを作成 - /// - private async Task CreateConfiguredReviewAgentAsync( - string owner, string repo, int prNumber) - { - var customPrompt = _config.AgentFramework?.Agents?.Review?.CustomSystemPrompt; - return await _agentFactory.CreateReviewAgentAsync(owner, repo, prNumber, customPrompt); - } - - /// - /// 設定を反映したSummaryエージェントを作成 - /// - private async Task CreateConfiguredSummaryAgentAsync( - string owner, string repo, int prNumber) - { - var customPrompt = _config.AgentFramework?.Agents?.Summary?.CustomSystemPrompt; - return await _agentFactory.CreateSummaryAgentAsync(owner, repo, prNumber, customPrompt); - } } diff --git a/PRAgent/appsettings.json b/PRAgent/appsettings.json index f5ae172..298a08e 100644 --- a/PRAgent/appsettings.json +++ b/PRAgent/appsettings.json @@ -38,11 +38,11 @@ "obj/**" ], "AgentFramework": { - "Enabled": false, + "Enabled": true, "OrchestrationMode": "sequential", "SelectionStrategy": "approval_workflow", "EnableFunctionCalling": true, - "EnableAutoApproval": false, + "EnableAutoApproval": true, "MaxTurns": 10 } }, From 008b537003205847a24c5dcf82c0dc0cc95967da Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:37:49 +0900 Subject: [PATCH 10/27] [fix] comment --- PRAgent/Agents/SK/SKApprovalAgent.cs | 6 ++- PRAgent/CommandLine/CommentCommandHandler.cs | 4 +- PRAgent/Models/PRActionBuffer.cs | 19 ++++++++- PRAgent/Plugins/GitHub/PostCommentFunction.cs | 41 +++++++++++++++++++ PRAgent/Services/GitHubService.cs | 18 +++++++- PRAgent/Services/IGitHubService.cs | 2 +- PRAgent/Services/PRActionExecutor.cs | 24 ++++++++--- .../Services/SK/SKAgentOrchestratorService.cs | 2 +- 8 files changed, 103 insertions(+), 13 deletions(-) diff --git a/PRAgent/Agents/SK/SKApprovalAgent.cs b/PRAgent/Agents/SK/SKApprovalAgent.cs index 6523bb7..d483080 100644 --- a/PRAgent/Agents/SK/SKApprovalAgent.cs +++ b/PRAgent/Agents/SK/SKApprovalAgent.cs @@ -1,6 +1,7 @@ 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; @@ -138,8 +139,8 @@ public async Task CreateAgentWithBufferAsync( // Kernelを作成してプラグインを登録 var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, customSystemPrompt); - kernel.ImportPluginFromObject(approvePlugin); - kernel.ImportPluginFromObject(commentPlugin); + kernel.ImportPluginFromObject(approvePlugin, "ApprovePR"); + kernel.ImportPluginFromObject(commentPlugin, "PostComment"); // エージェントを作成 var agent = new ChatCompletionAgent @@ -163,6 +164,7 @@ public async Task CreateAgentWithBufferAsync( string reviewResult, ApprovalThreshold threshold, bool autoApprove = false, + string? language = null, CancellationToken cancellationToken = default) { // バッファを作成 diff --git a/PRAgent/CommandLine/CommentCommandHandler.cs b/PRAgent/CommandLine/CommentCommandHandler.cs index 40aae95..b57c833 100644 --- a/PRAgent/CommandLine/CommentCommandHandler.cs +++ b/PRAgent/CommandLine/CommentCommandHandler.cs @@ -65,7 +65,9 @@ public async Task ExecuteAsync() .Where(c => c != null) .Select(c => ( FilePath: c!.FilePath, - LineNumber: c.LineNumber, + LineNumber: (int?)c.LineNumber, + StartLine: (int?)null, + EndLine: (int?)null, Comment: c.CommentText, Suggestion: c.SuggestionText )) diff --git a/PRAgent/Models/PRActionBuffer.cs b/PRAgent/Models/PRActionBuffer.cs index e87a9ed..5153650 100644 --- a/PRAgent/Models/PRActionBuffer.cs +++ b/PRAgent/Models/PRActionBuffer.cs @@ -27,6 +27,21 @@ public void AddLineComment(string filePath, int lineNumber, string comment, stri }); } + /// + /// 範囲コメントを追加します + /// + public void AddRangeComment(string filePath, int startLine, int endLine, string comment, string? suggestion = null) + { + _lineComments.Add(new LineCommentAction + { + FilePath = filePath, + StartLine = startLine, + EndLine = endLine, + Comment = comment, + Suggestion = suggestion + }); + } + /// /// レビューコメントを追加します /// @@ -130,7 +145,9 @@ public enum PRApprovalState public class LineCommentAction { public required string FilePath { get; init; } - public required int LineNumber { get; init; } + public int? LineNumber { get; init; } + public int? StartLine { get; init; } + public int? EndLine { get; init; } public required string Comment { get; init; } public string? Suggestion { get; init; } } diff --git a/PRAgent/Plugins/GitHub/PostCommentFunction.cs b/PRAgent/Plugins/GitHub/PostCommentFunction.cs index 6144de7..784ccea 100644 --- a/PRAgent/Plugins/GitHub/PostCommentFunction.cs +++ b/PRAgent/Plugins/GitHub/PostCommentFunction.cs @@ -61,6 +61,47 @@ public string PostLineCommentAsync( return $"Line comment has been added to buffer for {filePath}:{lineNumber}"; } + /// + /// プルリクエストの特定の範囲にコメントを追加します(バッファに追加) + /// + /// ファイルパス + /// 開始行番号 + /// 終了行番号 + /// コメント内容 + /// 提案される変更内容(オプション) + /// 範囲コメントがバッファに追加されたことを示すメッセージ + [KernelFunction("post_range_comment")] + public string PostRangeCommentAsync( + string filePath, + int startLine, + int endLine, + string comment, + string? suggestion = null) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + return "Error: File path cannot be empty"; + } + + if (startLine <= 0 || endLine <= 0) + { + return "Error: Line numbers must be positive"; + } + + if (startLine > endLine) + { + return "Error: startLine must be less than or equal to endLine"; + } + + if (string.IsNullOrWhiteSpace(comment)) + { + return "Error: Comment cannot be empty"; + } + + _buffer.AddRangeComment(filePath, startLine, endLine, comment, suggestion); + return $"Range comment has been added to buffer for {filePath}:{startLine}-{endLine}"; + } + /// /// レビューコメントを追加します(バッファに追加) /// diff --git a/PRAgent/Services/GitHubService.cs b/PRAgent/Services/GitHubService.cs index 1e9b917..960017a 100644 --- a/PRAgent/Services/GitHubService.cs +++ b/PRAgent/Services/GitHubService.cs @@ -173,12 +173,26 @@ public async Task CreateLineCommentAsync(string owner, string ); } - public async Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int LineNumber, string Comment, string? Suggestion)> comments) + public async Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)> comments) { var draftComments = comments.Select(c => { var commentBody = c.Suggestion != null ? $"{c.Comment}\n```suggestion\n{c.Suggestion}\n```" : c.Comment; - return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.LineNumber); + + // 1行コメントのみ対応(LineNumberがある場合) + if (c.LineNumber.HasValue) + { + return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.LineNumber.Value); + } + // 範囲コメントは1行目を使用 + 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(); return await _client.PullRequest.Review.Create( diff --git a/PRAgent/Services/IGitHubService.cs b/PRAgent/Services/IGitHubService.cs index 587a56a..5722ac1 100644 --- a/PRAgent/Services/IGitHubService.cs +++ b/PRAgent/Services/IGitHubService.cs @@ -11,7 +11,7 @@ public interface IGitHubService Task GetPullRequestDiffAsync(string owner, string repo, int prNumber); Task CreateReviewCommentAsync(string owner, string repo, int prNumber, string body); Task CreateLineCommentAsync(string owner, string repo, int prNumber, string filePath, int lineNumber, string comment, string? suggestion = null); - Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int LineNumber, string Comment, string? Suggestion)> comments); + Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)> comments); Task CreateIssueCommentAsync(string owner, string repo, int prNumber, string body); Task ApprovePullRequestAsync(string owner, string repo, int prNumber, string? comment = null); Task GetRepositoryFileContentAsync(string owner, string repo, string path, string? branch = null); diff --git a/PRAgent/Services/PRActionExecutor.cs b/PRAgent/Services/PRActionExecutor.cs index 8f5e0c3..ad011ee 100644 --- a/PRAgent/Services/PRActionExecutor.cs +++ b/PRAgent/Services/PRActionExecutor.cs @@ -40,12 +40,25 @@ public async Task ExecuteAsync(PRActionBuffer buffer, Cancellati try { - // 1. 行コメントを投稿 + // 1. レビューコメントを投稿 + if (buffer.ReviewComments.Count > 0) + { + foreach (var reviewComment in buffer.ReviewComments) + { + await _gitHubService.CreateReviewCommentAsync( + _owner, _repo, _prNumber, reviewComment.Comment); + } + result.ReviewCommentsPosted = buffer.ReviewComments.Count; + } + + // 2. 行コメントを投稿 if (buffer.LineComments.Count > 0) { var comments = buffer.LineComments.Select(c => ( c.FilePath, c.LineNumber, + c.StartLine, + c.EndLine, c.Comment, c.Suggestion )).ToList(); @@ -54,10 +67,9 @@ public async Task ExecuteAsync(PRActionBuffer buffer, Cancellati _owner, _repo, _prNumber, comments); result.LineCommentsPosted = comments.Count; - result.Success = true; } - // 2. サマリーを全体コメントとして投稿 + // 3. サマリーを全体コメントとして投稿 if (buffer.Summaries.Count > 0) { var summaryText = string.Join("\n\n", buffer.Summaries); @@ -71,7 +83,7 @@ public async Task ExecuteAsync(PRActionBuffer buffer, Cancellati result.SummaryCommentUrl = commentResult.HtmlUrl; } - // 3. 全体コメントを投稿 + // 4. 全体コメントを投稿 if (!string.IsNullOrEmpty(buffer.GeneralComment)) { var commentResult = await _gitHubService.CreateIssueCommentAsync( @@ -81,7 +93,7 @@ public async Task ExecuteAsync(PRActionBuffer buffer, Cancellati result.GeneralCommentUrl = commentResult.HtmlUrl; } - // 4. 承認ステータスに応じた処理を実行 + // 5. 承認ステータスに応じた処理を実行 switch (buffer.ApprovalState) { case PRApprovalState.Approved: @@ -109,6 +121,7 @@ await _gitHubService.CreateReviewCommentAsync( } result.TotalActionsPosted = + result.ReviewCommentsPosted + result.LineCommentsPosted + result.SummariesPosted + (result.GeneralCommentPosted ? 1 : 0) + @@ -202,6 +215,7 @@ public class PRActionResult public int PrNumber { get; init; } public bool Success { get; set; } public int TotalActionsPosted { get; set; } + public int ReviewCommentsPosted { get; set; } public int LineCommentsPosted { get; set; } public int SummariesPosted { get; set; } public bool GeneralCommentPosted { get; set; } diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index 65bd350..365970e 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -88,7 +88,7 @@ public async Task ReviewAndApproveAsync( { // FunctionCalling使用 - PRActionResultが返される var (_, reasoningFc, commentFc, actionResult) = await _approvalAgent.DecideWithFunctionCallingAsync( - owner, repo, prNumber, review, threshold, autoApprove, cancellationToken); + owner, repo, prNumber, review, threshold, autoApprove, language: null, cancellationToken); shouldApprove = actionResult?.Approved ?? false; reasoning = reasoningFc; comment = commentFc; From fc86bb66b6da25b12486559a91fe1d2313fa7126 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:41:37 +0900 Subject: [PATCH 11/27] [fix]endpoint --- PRAgent/Services/KernelService.cs | 53 +++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/PRAgent/Services/KernelService.cs b/PRAgent/Services/KernelService.cs index 42be95e..8f5e4b2 100644 --- a/PRAgent/Services/KernelService.cs +++ b/PRAgent/Services/KernelService.cs @@ -25,11 +25,24 @@ public Kernel CreateKernel(string? systemPrompt = null) { var builder = Kernel.CreateBuilder(); - builder.AddOpenAIChatCompletion( - modelId: _aiSettings.ModelId, - apiKey: _aiSettings.ApiKey, - endpoint: new Uri(_aiSettings.Endpoint) - ); + var endpoint = _aiSettings.Endpoint; + + // エンドポイントが指定されている場合はカスタムエンドポイントを使用 + if (!string.IsNullOrEmpty(endpoint) && endpoint != "https://api.openai.com/v1") + { + builder.Services.AddOpenAIChatCompletion( + modelId: _aiSettings.ModelId, + apiKey: _aiSettings.ApiKey, + endpoint: new Uri(endpoint) + ); + } + else + { + builder.AddOpenAIChatCompletion( + modelId: _aiSettings.ModelId, + apiKey: _aiSettings.ApiKey + ); + } var kernel = builder.Build(); @@ -40,20 +53,26 @@ public Kernel CreateAgentKernel(string? systemPrompt = null) { var builder = Kernel.CreateBuilder(); - builder.AddOpenAIChatCompletion( - modelId: _aiSettings.ModelId, - apiKey: _aiSettings.ApiKey, - endpoint: new Uri(_aiSettings.Endpoint) - ); + var endpoint = _aiSettings.Endpoint; - var kernel = builder.Build(); + // エンドポイントが指定されている場合はカスタムエンドポイントを使用 + if (!string.IsNullOrEmpty(endpoint) && endpoint != "https://api.openai.com/v1") + { + builder.Services.AddOpenAIChatCompletion( + modelId: _aiSettings.ModelId, + apiKey: _aiSettings.ApiKey, + endpoint: new Uri(endpoint) + ); + } + else + { + builder.AddOpenAIChatCompletion( + modelId: _aiSettings.ModelId, + apiKey: _aiSettings.ApiKey + ); + } - // 注: SetDefaultSystemPromptは現在のバージョンではまだ利用できない - // 将来的には以下のようにsystemPromptを設定できるようになる予定 - // if (!string.IsNullOrEmpty(systemPrompt)) - // { - // kernel.SetDefaultSystemPrompt(systemPrompt); - // } + var kernel = builder.Build(); return kernel; } From 4bc70e4e0672ba4ecd0b2c3a8070d15f2d1fed2d Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:44:03 +0900 Subject: [PATCH 12/27] [fix] endpoint --- PRAgent/Models/AISettings.cs | 2 +- PRAgent/Services/KernelService.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/PRAgent/Models/AISettings.cs b/PRAgent/Models/AISettings.cs index f40bdf1..c97b3b6 100644 --- a/PRAgent/Models/AISettings.cs +++ b/PRAgent/Models/AISettings.cs @@ -4,7 +4,7 @@ public class AISettings { public const string SectionName = "AISettings"; - public string Endpoint { get; set; } = "https://api.openai.com/v1"; + public string Endpoint { get; set; } = string.Empty; // デフォルトは空(環境変数で設定) public string ApiKey { get; set; } = string.Empty; public string ModelId { get; set; } = "gpt-4o-mini"; public int MaxTokens { get; set; } = 4000; diff --git a/PRAgent/Services/KernelService.cs b/PRAgent/Services/KernelService.cs index 8f5e4b2..9803cb0 100644 --- a/PRAgent/Services/KernelService.cs +++ b/PRAgent/Services/KernelService.cs @@ -28,8 +28,9 @@ public Kernel CreateKernel(string? systemPrompt = null) var endpoint = _aiSettings.Endpoint; // エンドポイントが指定されている場合はカスタムエンドポイントを使用 - if (!string.IsNullOrEmpty(endpoint) && endpoint != "https://api.openai.com/v1") + if (!string.IsNullOrEmpty(endpoint)) { + _logger?.LogInformation("Using custom endpoint: {Endpoint}", endpoint); builder.Services.AddOpenAIChatCompletion( modelId: _aiSettings.ModelId, apiKey: _aiSettings.ApiKey, @@ -38,6 +39,7 @@ public Kernel CreateKernel(string? systemPrompt = null) } else { + _logger?.LogInformation("Using default OpenAI endpoint"); builder.AddOpenAIChatCompletion( modelId: _aiSettings.ModelId, apiKey: _aiSettings.ApiKey @@ -56,8 +58,9 @@ public Kernel CreateAgentKernel(string? systemPrompt = null) var endpoint = _aiSettings.Endpoint; // エンドポイントが指定されている場合はカスタムエンドポイントを使用 - if (!string.IsNullOrEmpty(endpoint) && endpoint != "https://api.openai.com/v1") + if (!string.IsNullOrEmpty(endpoint)) { + _logger?.LogInformation("Using custom endpoint: {Endpoint}", endpoint); builder.Services.AddOpenAIChatCompletion( modelId: _aiSettings.ModelId, apiKey: _aiSettings.ApiKey, @@ -66,6 +69,7 @@ public Kernel CreateAgentKernel(string? systemPrompt = null) } else { + _logger?.LogInformation("Using default OpenAI endpoint"); builder.AddOpenAIChatCompletion( modelId: _aiSettings.ModelId, apiKey: _aiSettings.ApiKey From 3f877f1952ff6419e4db96ba70e9bf1134ff7eef Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:51:17 +0900 Subject: [PATCH 13/27] [fix] yml --- .github/workflows/pr-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index faff460..0ec213c 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -41,7 +41,7 @@ jobs: --language ja \ --post-comment env: - AISettings__Endpoint: ${{ vars.AI_ENDPOINT || 'https://api.openai.com/v1' }} + AISettings__Endpoint: ${{ vars.AISETTINGS__ENDPOINT }} AISettings__ApiKey: ${{ secrets.AI_API_KEY }} AISettings__ModelId: ${{ vars.AI_MODEL_ID || 'gpt-4o-mini' }} PRSettings__GitHubToken: ${{ secrets.GITHUB_TOKEN }} From 9033ed1507301df253c3dce975e3f94691177079 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:52:37 +0900 Subject: [PATCH 14/27] [fix] appsettings --- PRAgent/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRAgent/appsettings.json b/PRAgent/appsettings.json index 298a08e..ac4516c 100644 --- a/PRAgent/appsettings.json +++ b/PRAgent/appsettings.json @@ -1,6 +1,6 @@ { "AISettings": { - "Endpoint": "https://api.openai.com/v1", + "Endpoint": "", "ApiKey": "", "ModelId": "gpt-4o-mini", "MaxTokens": 4000, From e5e080ebb1b32ac476624420b504c39fa6e09e83 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:52:45 +0900 Subject: [PATCH 15/27] fix --- PRAgent/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRAgent/appsettings.json b/PRAgent/appsettings.json index ac4516c..727c2b5 100644 --- a/PRAgent/appsettings.json +++ b/PRAgent/appsettings.json @@ -2,7 +2,7 @@ "AISettings": { "Endpoint": "", "ApiKey": "", - "ModelId": "gpt-4o-mini", + "ModelId": "", "MaxTokens": 4000, "Temperature": 0.7 }, From 0e917d00f496dcd90707c3a56b7906417f74c25e Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 01:56:12 +0900 Subject: [PATCH 16/27] [fix] --- .github/workflows/pr-review.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 0ec213c..7e897ca 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -42,6 +42,6 @@ jobs: --post-comment env: AISettings__Endpoint: ${{ vars.AISETTINGS__ENDPOINT }} - AISettings__ApiKey: ${{ secrets.AI_API_KEY }} - AISettings__ModelId: ${{ vars.AI_MODEL_ID || 'gpt-4o-mini' }} + AISettings__ApiKey: ${{ secrets.AISETTINGS__APIKEY }} + AISettings__ModelId: ${{ vars.AISETTINGS__MODELID || 'gpt-4o-mini' }} PRSettings__GitHubToken: ${{ secrets.GITHUB_TOKEN }} From 7a3794cb5358b297df949c92ea83c380505ca879 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:02:16 +0900 Subject: [PATCH 17/27] [fix] calling --- PRAgent/Agents/AgentDefinition.cs | 24 ++-- PRAgent/Agents/SK/SKApprovalAgent.cs | 183 ++++++++++++++++++++++----- PRAgent/Models/AISettings.cs | 2 +- PRAgent/appsettings.json | 4 +- 4 files changed, 167 insertions(+), 46 deletions(-) diff --git a/PRAgent/Agents/AgentDefinition.cs b/PRAgent/Agents/AgentDefinition.cs index 4ab7b24..af16b7f 100644 --- a/PRAgent/Agents/AgentDefinition.cs +++ b/PRAgent/Agents/AgentDefinition.cs @@ -218,21 +218,21 @@ 4. Detailed comment body with suggestions 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 + あなたの役割: + 1. コードレビュー結果を分析 + 2. 承認基準に照らして評価 + 3. 保守的でリスクを考慮した承認決定を行う + 4. 判断について明確な理由を提供 - 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 + 承認基準: + - critical: 重大な問題が0件であること + - major: 重大または重大な問題が0件であること + - minor: 軽微、重大、重大な問題が0件であること + - none: 常に承認 - 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" ); diff --git a/PRAgent/Agents/SK/SKApprovalAgent.cs b/PRAgent/Agents/SK/SKApprovalAgent.cs index d483080..4a00abd 100644 --- a/PRAgent/Agents/SK/SKApprovalAgent.cs +++ b/PRAgent/Agents/SK/SKApprovalAgent.cs @@ -131,7 +131,8 @@ public async Task CreateAgentWithBufferAsync( string repo, int prNumber, PRActionBuffer buffer, - string? customSystemPrompt = null) + string? customSystemPrompt = null, + string? language = null) { // プラグインインスタンスを作成 var approvePlugin = new ApprovePRFunction(buffer); @@ -139,21 +140,103 @@ public async Task CreateAgentWithBufferAsync( // Kernelを作成してプラグインを登録 var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, customSystemPrompt); - kernel.ImportPluginFromObject(approvePlugin, "ApprovePR"); - kernel.ImportPluginFromObject(commentPlugin, "PostComment"); + kernel.ImportPluginFromObject(approvePlugin); + kernel.ImportPluginFromObject(commentPlugin); + + // 言語に応じたシステムプロンプトを作成 + var systemPrompt = customSystemPrompt ?? GetSystemPrompt(language); // エージェントを作成 var agent = new ChatCompletionAgent { Name = AgentDefinition.ApprovalAgent.Name, Description = AgentDefinition.ApprovalAgent.Description, - Instructions = customSystemPrompt ?? AgentDefinition.ApprovalAgent.SystemPrompt, + Instructions = systemPrompt, Kernel = kernel }; return await Task.FromResult(agent); } + /// + /// 言語に応じたシステムプロンプトを取得します + /// + private static string GetSystemPrompt(string? language) + { + var isJapanese = language?.ToLowerInvariant() == "ja"; + + if (isJapanese) + { + return $""" + あなたはプルリクエストの承認決定を行うシニアテクニカルリードです。 + + あなたの役割: + 1. コードレビュー結果を分析 + 2. 承認基準に照らして評価 + 3. 保守的でリスクを考慮した承認決定を行う + 4. 判断について明確な理由を提供 + + ## 利用可能な関数 + 以下の関数を呼び出してアクションをバッファに追加してください: + - approve_pull_request - PRを承認 + - request_changes - 変更を依頼 + - post_pr_comment - 全体コメントを追加 + - post_line_comment - 特定行にコメントを追加 + - post_range_comment - 複数行にコメントを追加(開始行と終了行を指定) + - post_review_comment - レビューレベルのコメントを追加 + + ## 関数呼び出しの方法 + 関数を呼び出すには、関数名と必要なパラメータを明記してください: + 例: 「approve_pull_request関数を呼び出します。コメント: 良好です」 + + すべてのアクションはバッファに追加され、分析完了後に一括でGitHubに投稿されます。 + + ## 承認基準 + - critical: 重大な問題が0件であること + - major: 重大または重大な問題が0件であること + - minor: 軽微、重大、重大な問題が0件であること + - none: 常に承認 + + 不確実な場合は、慎重を期して追加レビューまたは変更依頼を推奨します。 + """; + } + else + { + return $""" + 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 + + ## Available Functions + Call the following functions to add actions to buffer: + - approve_pull_request - Approve the PR + - request_changes - Request changes + - post_pr_comment - Add a general comment + - post_line_comment - Add a comment to a specific line + - post_range_comment - Add a comment to a range of lines + - post_review_comment - Add a review-level comment + + ## How to Call Functions + Explicitly state that you are calling a function with its parameters: + Example: "I will call the approve_pull_request function with comment: Looks good." + + All actions will be buffered and posted to GitHub after your analysis is complete. + + ## 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. + """; + } + } + /// /// バッファリングパターンを使用して決定を行い、完了後にアクションを一括実行します /// @@ -170,8 +253,8 @@ public async Task CreateAgentWithBufferAsync( // バッファを作成 var buffer = new PRActionBuffer(); - // バッファを使用したエージェントを作成 - var agent = await CreateAgentWithBufferAsync(owner, repo, prNumber, buffer); + // バッファを使用したエージェントを作成(言語指定) + var agent = await CreateAgentWithBufferAsync(owner, repo, prNumber, buffer, language: language); // PR情報を取得 var pr = await _gitHubService.GetPullRequestAsync(owner, repo, prNumber); @@ -179,52 +262,90 @@ public async Task CreateAgentWithBufferAsync( // プロンプトを作成 var autoApproveInstruction = autoApprove - ? "If the decision is to APPROVE, use the approve_pull_request function to add approval to buffer." - : "If the decision is to APPROVE, clearly state DECISION: APPROVE in your response."; + ? "判断が承認(APPROVE)の場合は、approve_pull_request関数を呼び出して承認をバッファに追加してください。" + : "判断が承認(APPROVE)の場合は、DECISION: APPROVEと明確に記載してください。"; 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} + ## プルリクエスト + - タイトル: {pr.Title} + - 作成者: {pr.User.Login} - ## Code Review Result + ## コードレビュー結果 {reviewResult} - ## Approval Threshold + ## 承認基準 {thresholdDescription} - Your task: - 1. Analyze the review results against the approval threshold - 2. Make a decision (APPROVE, CHANGES_REQUESTED, or COMMENT_ONLY) + あなたのタスク: + 1. レビュー結果を承認基準と照らして分析 + 2. 判断を下してください(APPROVE、CHANGES_REQUESTED、COMMENT_ONLYのいずれか) 3. {autoApproveInstruction} - 4. If the decision is CHANGES_REQUESTED, use the request_changes function to add changes requested to buffer. - 5. If there are specific concerns that need to be addressed, use: - - post_pr_comment for general comments - - post_line_comment for specific line-level feedback - - post_review_comment for review-level comments + 4. 判断がCHANGES_REQUESTEDの場合、request_changes関数を呼び出して変更依頼をバッファに追加してください + 5. 対処すべき懸念事項がある場合は、以下の関数を呼び出してください: + - post_pr_comment - 全般的なコメント + - post_line_comment - 特定行へのフィードバック + - post_range_comment - 複数行へのフィードバック(開始行と終了行を指定) + - post_review_comment - レビューレベルのコメント - All actions will be buffered and executed after your analysis is complete. + 重要: すべてのアクションはバッファに追加され、分析完了後に一括で実行されます。 - Provide your decision in this format: + 以下の形式で判断を記載してください: DECISION: [APPROVE/CHANGES_REQUESTED/COMMENT_ONLY] - 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] + REASONING: [判断理由を説明] + CONDITIONS: [マージ条件があれば記載、なければN/A] + APPROVAL_COMMENT: [承認時のコメント、なければN/A] - Be conservative - when in doubt, request changes or add comments. + 不確かな場合は、変更依頼またはコメントを追加してください。 """; var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(prompt); - // エージェントを実行(関数呼び出しはバッファに追加される) + // FunctionCallingを有効にするために、Kernelから直接サービスを呼び出し + var kernel = agent.Kernel; + var chatService = kernel.GetRequiredService(); + + // OpenAI用の実行設定でFunctionCallingを有効化 + var executionSettings = new OpenAIPromptExecutionSettings + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() + }; + var responses = new System.Text.StringBuilder(); - await foreach (var response in agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken)) + + // 関数呼び出しを含む可能性があるため、複数回の反復処理を行う + var maxIterations = 10; + var iteration = 0; + + while (iteration < maxIterations) { - responses.Append(response.Message.Content); + iteration++; + + // 非ストリーミングAPIで完全なレスポンスを取得 + var contents = await chatService.GetChatMessageContentsAsync( + chatHistory, + executionSettings, + kernel, + cancellationToken); + + var content = contents.FirstOrDefault(); + if (content == null) break; + + var currentResponse = content.Content ?? string.Empty; + responses.Append(currentResponse); + chatHistory.AddAssistantMessage(currentResponse); + + // 関数呼び出しが行われたかチェック + var hasFunctionCalls = content.Items?.Any(i => i is FunctionCallContent) == true; + + // 関数呼び出しがない場合はループを抜ける + if (!hasFunctionCalls) + { + break; + } } var responseText = responses.ToString(); diff --git a/PRAgent/Models/AISettings.cs b/PRAgent/Models/AISettings.cs index c97b3b6..e91d681 100644 --- a/PRAgent/Models/AISettings.cs +++ b/PRAgent/Models/AISettings.cs @@ -4,7 +4,7 @@ public class AISettings { public const string SectionName = "AISettings"; - public string Endpoint { get; set; } = string.Empty; // デフォルトは空(環境変数で設定) + public string Endpoint { get; set; } = "https://api.openai.com/v1"; // デフォルトはOpenAI(環境変数で上書き可能) public string ApiKey { get; set; } = string.Empty; public string ModelId { get; set; } = "gpt-4o-mini"; public int MaxTokens { get; set; } = 4000; diff --git a/PRAgent/appsettings.json b/PRAgent/appsettings.json index 727c2b5..298a08e 100644 --- a/PRAgent/appsettings.json +++ b/PRAgent/appsettings.json @@ -1,8 +1,8 @@ { "AISettings": { - "Endpoint": "", + "Endpoint": "https://api.openai.com/v1", "ApiKey": "", - "ModelId": "", + "ModelId": "gpt-4o-mini", "MaxTokens": 4000, "Temperature": 0.7 }, From 128253d599c6e99b6883ce45a818fb11589b830d Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:18:25 +0900 Subject: [PATCH 18/27] [fix] Comment --- PRAgent.Tests/packages.lock.json | 79 ++++++++++++++++++++++++++ PRAgent/Agents/DetailedCommentAgent.cs | 4 +- PRAgent/packages.lock.json | 79 ++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) 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/DetailedCommentAgent.cs b/PRAgent/Agents/DetailedCommentAgent.cs index c975fec..208bff0 100644 --- a/PRAgent/Agents/DetailedCommentAgent.cs +++ b/PRAgent/Agents/DetailedCommentAgent.cs @@ -147,14 +147,14 @@ private List ExtractIssuesFromReview(string review) _logger.LogInformation("=== DetailedCommentAgent Response ===\n{Response}", aiResponse); - return new DraftPullRequestReviewComment(issue.FilePath, aiResponse, issue.StartLine); + return new DraftPullRequestReviewComment(aiResponse, issue.FilePath, issue.StartLine); } catch { // フォールバック:簡潔なコメントを作成 return new DraftPullRequestReviewComment( - issue.FilePath, $"{issue.Level}: {issue.Description}\n\n{issue.Suggestion}", + issue.FilePath, issue.StartLine); } } diff --git a/PRAgent/packages.lock.json b/PRAgent/packages.lock.json index db03f5c..84795ca 100644 --- a/PRAgent/packages.lock.json +++ b/PRAgent/packages.lock.json @@ -94,6 +94,29 @@ "Microsoft.SemanticKernel.Core": "1.68.0" } }, + "Microsoft.SemanticKernel.Agents.Core": { + "type": "Direct", + "requested": "[1.68.0, )", + "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": "Direct", + "requested": "[1.68.0-preview, )", + "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.OpenAI": { "type": "Direct", "requested": "[1.68.0, )", @@ -173,6 +196,26 @@ "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" + } + }, + "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", @@ -429,6 +472,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.Abstractions": { "type": "Transitive", "resolved": "1.68.0", @@ -439,6 +504,15 @@ "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.Connectors.AzureOpenAI": { "type": "Transitive", "resolved": "1.68.0", @@ -504,6 +578,11 @@ "type": "Transitive", "resolved": "10.0.0", "contentHash": "PxZKFx8ACAt7gRnTSDgtZFA06cimls5XL1Olv63bWF8qKL5+EQdqq3K02etW4LNut4xDoREPe9z7QYATLMkBPA==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==" } } } From 6c0f2dcb1b49539bb2a67951f2ac31154e19e8d0 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:21:13 +0900 Subject: [PATCH 19/27] [add] test --- .../DraftPullRequestReviewCommentTests.cs | 112 ++++++++++++++ .../GitHubServiceLineCommentTests.cs | 145 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 PRAgent.Tests/DraftPullRequestReviewCommentTests.cs create mode 100644 PRAgent.Tests/GitHubServiceLineCommentTests.cs 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); + } +} From 5fef0088ec540b505bf1cf5ba7950a3fbec06cec Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:29:43 +0900 Subject: [PATCH 20/27] [fix] subagent --- PRAgent/Agents/SK/SKApprovalAgent.cs | 2 +- PRAgent/Agents/SK/SKReviewAgent.cs | 191 +++++++++++++++++- .../Services/SK/SKAgentOrchestratorService.cs | 20 ++ 3 files changed, 211 insertions(+), 2 deletions(-) diff --git a/PRAgent/Agents/SK/SKApprovalAgent.cs b/PRAgent/Agents/SK/SKApprovalAgent.cs index 4a00abd..2954c43 100644 --- a/PRAgent/Agents/SK/SKApprovalAgent.cs +++ b/PRAgent/Agents/SK/SKApprovalAgent.cs @@ -106,7 +106,7 @@ public async Task ApproveAsync( CancellationToken cancellationToken = default) { // まずReviewエージェントを呼び出してレビューを実行 - var reviewAgent = new SKReviewAgent(_agentFactory, _prDataService); + var reviewAgent = new SKReviewAgent(_agentFactory, _prDataService, _gitHubService); var reviewResult = await reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); // 決定を行う diff --git a/PRAgent/Agents/SK/SKReviewAgent.cs b/PRAgent/Agents/SK/SKReviewAgent.cs index 811481f..30dd051 100644 --- a/PRAgent/Agents/SK/SKReviewAgent.cs +++ b/PRAgent/Agents/SK/SKReviewAgent.cs @@ -1,7 +1,10 @@ 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; @@ -13,13 +16,16 @@ public class SKReviewAgent { private readonly PRAgentFactory _agentFactory; private readonly PullRequestDataService _prDataService; + private readonly IGitHubService _gitHubService; public SKReviewAgent( PRAgentFactory agentFactory, - PullRequestDataService prDataService) + PullRequestDataService prDataService, + IGitHubService gitHubService) { _agentFactory = agentFactory; _prDataService = prDataService; + _gitHubService = gitHubService; } /// @@ -100,4 +106,187 @@ public async Task CreateAgentWithFunctionsAsync( return await _agentFactory.CreateReviewAgentAsync( owner, repo, prNumber, customSystemPrompt, functions); } + + /// + /// バッファを使用して行コメント付きレビューを実行します + /// メインコメントは簡潔に保ち、詳細なフィードバックは行コメントとして投稿します + /// + public async Task<(string ReviewText, PRActionResult? ActionResult)> ReviewWithLineCommentsAsync( + string owner, + string repo, + int prNumber, + string? language = null, + CancellationToken cancellationToken = default) + { + // バッファを作成 + var buffer = new PRActionBuffer(); + + // プラグインインスタンスを作成 + var commentPlugin = new PostCommentFunction(buffer); + + // カスタムシステムプロンプトを作成(簡潔なメインコメント+行コメント重視) + var systemPrompt = GetReviewWithLineCommentsPrompt(language); + + // Kernelを作成してプラグインを登録 + var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, systemPrompt); + kernel.ImportPluginFromObject(commentPlugin); + + // エージェントを作成 + var agent = new ChatCompletionAgent + { + Name = AgentDefinition.ReviewAgent.Name, + Description = AgentDefinition.ReviewAgent.Description, + Instructions = systemPrompt, + Kernel = kernel + }; + + // PRデータを取得 + var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); + var fileList = PullRequestDataService.FormatFileList(files); + + // プロンプトを作成 + var prompt = $""" + 以下のプルリクエストをコードレビューしてください。 + + ## プルリクエスト情報 + - タイトル: {pr.Title} + - 作成者: {pr.User.Login} + - 説明: {pr.Body} + + ## 変更されたファイル + {fileList} + + ## 差分 + {diff} + + ## レビュー指示 + 1. まず、post_review_comment関数を呼び出して、簡潔な全体レビュー(3-5行程度)を追加してください + 2. 各問題点に対して、post_line_comment関数を呼び出して行コメントを投稿してください + 3. 行コメントにはファイルパスと行番号を正確に指定してください + 4. 重大な問題には Critical、重要な問題には Major、軽微な問題には Minor のプレフィックスを付けてください + + 重要: メインのレビューコメントは簡潔に保ち、詳細なフィードバックは行コメントとして投稿してください。 + """; + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage(prompt); + + // FunctionCallingを有効にするために、Kernelから直接サービスを呼び出し + var chatService = kernel.GetRequiredService(); + + // OpenAI用の実行設定でFunctionCallingを有効化 + var executionSettings = new OpenAIPromptExecutionSettings + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() + }; + + var responses = new System.Text.StringBuilder(); + + // 関数呼び出しを含む可能性があるため、複数回の反復処理を行う + var maxIterations = 15; + var iteration = 0; + + while (iteration < maxIterations) + { + iteration++; + + var contents = await chatService.GetChatMessageContentsAsync( + chatHistory, + executionSettings, + kernel, + cancellationToken); + + var content = contents.FirstOrDefault(); + if (content == null) break; + + var currentResponse = content.Content ?? string.Empty; + responses.Append(currentResponse); + chatHistory.AddAssistantMessage(currentResponse); + + // 関数呼び出しが行われたかチェック + var hasFunctionCalls = content.Items?.Any(i => i is FunctionCallContent) == true; + + if (!hasFunctionCalls) + { + break; + } + } + + var reviewText = responses.ToString(); + + // バッファの内容を実行 + PRActionResult? actionResult = null; + var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber); + var state = buffer.GetState(); + + if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || state.HasGeneralComment) + { + actionResult = await executor.ExecuteAsync(buffer, cancellationToken); + } + + return (reviewText, actionResult); + } + + /// + /// 言語に応じた行コメント重視のレビュープロンプトを取得します + /// + private static string GetReviewWithLineCommentsPrompt(string? language) + { + var isJapanese = language?.ToLowerInvariant() == "ja"; + + if (isJapanese) + { + return """ + あなたはシニアソフトウェアエンジニアとしてプルリクエストのコードレビューを行います。 + + ## 重要なルール + 1. メインのレビューコメントは簡潔に(3-5行程度) + 2. 詳細なフィードバックは行コメントとして投稿 + 3. 各問題点に対して個別の行コメントを作成 + + ## 利用可能な関数 + - post_review_comment: 全体的なレビューコメント(簡潔に) + - post_line_comment: 特定の行にコメント(filePath, lineNumber, comment) + - post_range_comment: 複数行にコメント(filePath, startLine, endLine, comment) + - post_pr_comment: 全般的なコメント + + ## コメントの分類 + - [Critical]: 重大なバグ、セキュリティ問題 + - [Major]: 設計問題、パフォーマンス問題 + - [Minor]: スタイル、命名、軽微な改善 + + ## 出力形式 + 1. まず post_review_comment で簡潔な全体レビューを投稿 + 2. 各問題点に対して post_line_comment で行コメントを投稿 + 3. ファイルパスと行番号を正確に指定すること + """; + } + else + { + return """ + You are a senior software engineer performing code reviews on pull requests. + + ## Important Rules + 1. Keep the main review comment concise (3-5 lines) + 2. Post detailed feedback as line comments + 3. Create individual line comments for each issue + + ## Available Functions + - post_review_comment: Overall review comment (keep concise) + - post_line_comment: Comment on specific line (filePath, lineNumber, comment) + - post_range_comment: Comment on multiple lines (filePath, startLine, endLine, comment) + - post_pr_comment: General comment + + ## Issue Classification + - [Critical]: Critical bugs, security issues + - [Major]: Design issues, performance problems + - [Minor]: Style, naming, minor improvements + + ## Output Format + 1. First, post a concise overall review using post_review_comment + 2. For each issue, post a line comment using post_line_comment + 3. Ensure filePath and lineNumber are accurate + """; + } + } } diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index 365970e..e850a27 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -51,6 +51,26 @@ public SKAgentOrchestratorService( /// public async Task ReviewAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default) { + // FunctionCalling設定に応じてメソッドを選択 + var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + + if (useFunctionCalling) + { + // 行コメント付きレビューを実行 + var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( + owner, repo, prNumber, language: null, cancellationToken); + + if (actionResult != null) + { + _logger.LogInformation( + "Review with line comments completed. Line comments: {LineComments}, Review comments: {ReviewComments}", + actionResult.LineCommentsPosted, + actionResult.ReviewCommentsPosted); + } + + return reviewText; + } + return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); } From a871f559b456e5eb40b817ce2e79bb560bf44b61 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:33:03 +0900 Subject: [PATCH 21/27] [fix] SKReviewAgent --- PRAgent/Agents/SK/SKReviewAgent.cs | 50 +++++++----------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/PRAgent/Agents/SK/SKReviewAgent.cs b/PRAgent/Agents/SK/SKReviewAgent.cs index 30dd051..72e0995 100644 --- a/PRAgent/Agents/SK/SKReviewAgent.cs +++ b/PRAgent/Agents/SK/SKReviewAgent.cs @@ -131,13 +131,20 @@ public async Task CreateAgentWithFunctionsAsync( var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, systemPrompt); kernel.ImportPluginFromObject(commentPlugin); - // エージェントを作成 + // OpenAI用の実行設定でFunctionCallingを有効化 + var executionSettings = new OpenAIPromptExecutionSettings + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() + }; + + // エージェントを作成(ArgumentsでFunctionCallingを有効化) var agent = new ChatCompletionAgent { Name = AgentDefinition.ReviewAgent.Name, Description = AgentDefinition.ReviewAgent.Description, Instructions = systemPrompt, - Kernel = kernel + Kernel = kernel, + Arguments = new KernelArguments(executionSettings) }; // PRデータを取得 @@ -171,45 +178,12 @@ 4. 重大な問題には Critical、重要な問題には Major、軽微な問 var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(prompt); - // FunctionCallingを有効にするために、Kernelから直接サービスを呼び出し - var chatService = kernel.GetRequiredService(); - - // OpenAI用の実行設定でFunctionCallingを有効化 - var executionSettings = new OpenAIPromptExecutionSettings - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() - }; - var responses = new System.Text.StringBuilder(); - // 関数呼び出しを含む可能性があるため、複数回の反復処理を行う - var maxIterations = 15; - var iteration = 0; - - while (iteration < maxIterations) + // エージェントを実行(Function Callingは自動的に処理される) + await foreach (var response in agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken)) { - iteration++; - - var contents = await chatService.GetChatMessageContentsAsync( - chatHistory, - executionSettings, - kernel, - cancellationToken); - - var content = contents.FirstOrDefault(); - if (content == null) break; - - var currentResponse = content.Content ?? string.Empty; - responses.Append(currentResponse); - chatHistory.AddAssistantMessage(currentResponse); - - // 関数呼び出しが行われたかチェック - var hasFunctionCalls = content.Items?.Any(i => i is FunctionCallContent) == true; - - if (!hasFunctionCalls) - { - break; - } + responses.Append(response.Message.Content); } var reviewText = responses.ToString(); From f93d8d8ccb89dcaea5e684ea8e6a976534e9bbe0 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:39:56 +0900 Subject: [PATCH 22/27] [add] retry --- PRAgent/Services/GitHubService.cs | 98 +++++++++----- PRAgent/Services/KernelService.cs | 24 ++-- PRAgent/Services/RetryHelper.cs | 206 ++++++++++++++++++++++++++++++ 3 files changed, 286 insertions(+), 42 deletions(-) create mode 100644 PRAgent/Services/RetryHelper.cs diff --git a/PRAgent/Services/GitHubService.cs b/PRAgent/Services/GitHubService.cs index 960017a..c3679f4 100644 --- a/PRAgent/Services/GitHubService.cs +++ b/PRAgent/Services/GitHubService.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using Octokit; using PRAgent.Models; @@ -6,23 +7,31 @@ namespace PRAgent.Services; public class GitHubService : IGitHubService { private readonly GitHubClient _client; + private readonly ILogger? _logger; - public GitHubService(string gitHubToken) + public GitHubService(string gitHubToken, ILogger? logger = null) { _client = new GitHubClient(new ProductHeaderValue("PRAgent")) { Credentials = new Credentials(gitHubToken) }; + _logger = logger; } public async Task GetPullRequestAsync(string owner, string repo, int prNumber) { - return await _client.PullRequest.Get(owner, repo, prNumber); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Get(owner, repo, prNumber), + nameof(GetPullRequestAsync), + _logger); } public async Task> GetPullRequestFilesAsync(string owner, string repo, int prNumber) { - return await _client.PullRequest.Files(owner, repo, prNumber); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Files(owner, repo, prNumber), + nameof(GetPullRequestFilesAsync), + _logger); } public async Task> GetPullRequestCommentsAsync(string owner, string repo, int prNumber) @@ -34,7 +43,10 @@ public async Task> GetPullRequestComment public async Task> GetPullRequestReviewCommentsAsync(string owner, string repo, int prNumber) { - return await _client.Issue.Comment.GetAllForIssue(owner, repo, prNumber); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.Issue.Comment.GetAllForIssue(owner, repo, prNumber), + nameof(GetPullRequestReviewCommentsAsync), + _logger); } public async Task GetPullRequestDiffAsync(string owner, string repo, int prNumber) @@ -75,7 +87,10 @@ public async Task CreateReviewCommentAsync(string owner, stri Event = PullRequestReviewEvent.Comment }; - return await _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment), + nameof(CreateReviewCommentAsync), + _logger); } public async Task CreateReviewWithCommentsAsync(string owner, string repo, int prNumber, string reviewBody, List comments) @@ -87,7 +102,10 @@ public async Task CreateReviewWithCommentsAsync(string owner, Comments = comments }; - return await _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment), + nameof(CreateReviewWithCommentsAsync), + _logger); } /// @@ -102,12 +120,18 @@ public async Task CreateCompleteReviewAsync(string owner, string repo, int prNum Comments = comments }; - await _client.PullRequest.Review.Create(owner, repo, prNumber, review); + await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), + nameof(CreateCompleteReviewAsync), + _logger); } public async Task CreateIssueCommentAsync(string owner, string repo, int prNumber, string body) { - return await _client.Issue.Comment.Create(owner, repo, prNumber, body); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.Issue.Comment.Create(owner, repo, prNumber, body), + nameof(CreateIssueCommentAsync), + _logger); } public async Task ApprovePullRequestAsync(string owner, string repo, int prNumber, string? comment = null) @@ -118,17 +142,27 @@ public async Task ApprovePullRequestAsync(string owner, strin Event = PullRequestReviewEvent.Approve }; - return await _client.PullRequest.Review.Create(owner, repo, prNumber, review); + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), + nameof(ApprovePullRequestAsync), + _logger); } public async Task GetRepositoryFileContentAsync(string owner, string repo, string path, string? branch = null) { try { - var defaultBranch = await _client.Repository.Get(owner, repo); + var defaultBranch = await RetryHelper.ExecuteWithRetryAsync( + () => _client.Repository.Get(owner, repo), + nameof(GetRepositoryFileContentAsync) + "_GetRepository", + _logger); + var reference = branch ?? $"heads/{defaultBranch.DefaultBranch}"; - var contents = await _client.Repository.Content.GetAllContentsByRef(owner, repo, path, reference); + var contents = await RetryHelper.ExecuteWithRetryAsync( + () => _client.Repository.Content.GetAllContentsByRef(owner, repo, path, reference), + nameof(GetRepositoryFileContentAsync) + "_GetContents", + _logger); if (contents.Count > 0) { @@ -158,19 +192,19 @@ public async Task CreateLineCommentAsync(string owner, string // 行コメントを作成 var commentBody = suggestion != null ? $"{comment}\n```suggestion\n{suggestion}\n```" : comment; - return await _client.PullRequest.Review.Create( - owner, - repo, - prNumber, - new PullRequestReviewCreate + var review = new PullRequestReviewCreate + { + Event = PullRequestReviewEvent.Comment, + Comments = new List { - Event = PullRequestReviewEvent.Comment, - Comments = new List - { - new DraftPullRequestReviewComment(commentBody, filePath, lineNumber) - } + new DraftPullRequestReviewComment(commentBody, filePath, lineNumber) } - ); + }; + + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), + nameof(CreateLineCommentAsync), + _logger); } public async Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)> comments) @@ -195,15 +229,15 @@ public async Task CreateMultipleLineCommentsAsync(string owne } }).ToList(); - return await _client.PullRequest.Review.Create( - owner, - repo, - prNumber, - new PullRequestReviewCreate - { - Event = PullRequestReviewEvent.Comment, - Comments = draftComments - } - ); + var review = new PullRequestReviewCreate + { + Event = PullRequestReviewEvent.Comment, + Comments = draftComments + }; + + return await RetryHelper.ExecuteWithRetryAsync( + () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), + nameof(CreateMultipleLineCommentsAsync), + _logger); } } diff --git a/PRAgent/Services/KernelService.cs b/PRAgent/Services/KernelService.cs index 9803cb0..d848246 100644 --- a/PRAgent/Services/KernelService.cs +++ b/PRAgent/Services/KernelService.cs @@ -110,17 +110,17 @@ public async IAsyncEnumerable InvokePromptAsync( string prompt, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) { - // プロンプットを出力 + // プロンプトを出力 _logger?.LogInformation("=== KernelService Prompt ===\n{Prompt}", prompt); var service = kernel.GetRequiredService(); var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(prompt); + // ストリーミングを実行(リトライなし) await foreach (var content in service.GetStreamingChatMessageContentsAsync(chatHistory, cancellationToken: cancellationToken)) { - var responseContent = content.Content ?? string.Empty; - yield return responseContent; + yield return content.Content ?? string.Empty; } _logger?.LogInformation("=== KernelService Response (Streaming) ===\n{Response}", ""); @@ -131,17 +131,21 @@ public async Task InvokePromptAsStringAsync( string prompt, CancellationToken cancellationToken = default) { - // プロンプットを出力 + // プロンプトを出力 _logger?.LogInformation("=== KernelService Prompt ===\n{Prompt}", prompt); - var resultBuilder = new System.Text.StringBuilder(); + var service = kernel.GetRequiredService(); + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage(prompt); - await foreach (var content in InvokePromptAsync(kernel, prompt, cancellationToken)) - { - resultBuilder.Append(content); - } + // リトライ付きでストリーミングを実行して結果を収集 + var results = await RetryHelper.ExecuteStreamingWithRetryAsync( + () => service.GetStreamingChatMessageContentsAsync(chatHistory, cancellationToken: cancellationToken), + "InvokePromptAsync", + _logger, + cancellationToken); - var response = resultBuilder.ToString(); + var response = string.Join("", results.Select(c => c.Content ?? string.Empty)); _logger?.LogInformation("=== KernelService Response ===\n{Response}", response); return response; diff --git a/PRAgent/Services/RetryHelper.cs b/PRAgent/Services/RetryHelper.cs new file mode 100644 index 0000000..d6d16ad --- /dev/null +++ b/PRAgent/Services/RetryHelper.cs @@ -0,0 +1,206 @@ +using System.Net; +using Microsoft.Extensions.Logging; + +namespace PRAgent.Services; + +/// +/// HTTP 429エラー時のリトライ処理を提供するヘルパークラス +/// +public static class RetryHelper +{ + private const int MaxRetries = 30; + private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); + + /// + /// 429エラー時にリトライを行いながら非同期操作を実行します + /// + /// 戻り値の型 + /// 実行する操作 + /// 操作名(ログ用) + /// ロガー + /// キャンセルトークン + /// 操作の結果 + public static async Task ExecuteWithRetryAsync( + Func> operation, + string operationName, + ILogger? logger = null, + CancellationToken cancellationToken = default) + { + var attempt = 0; + + while (true) + { + attempt++; + + try + { + return await operation(); + } + catch (Exception ex) when (IsRetryableError(ex)) + { + if (attempt >= MaxRetries) + { + logger?.LogError( + "{OperationName} failed after {MaxRetries} attempts. Last error: {ErrorMessage}", + operationName, MaxRetries, ex.Message); + throw; + } + + var retryDelay = GetRetryDelay(ex); + + logger?.LogWarning( + "{OperationName} attempt {Attempt}/{MaxRetries} failed with retryable error: {ErrorMessage}. " + + "Waiting {DelaySeconds} seconds before retry...", + operationName, attempt, MaxRetries, ex.Message, retryDelay.TotalSeconds); + + await Task.Delay(retryDelay, cancellationToken); + } + } + } + + /// + /// 429エラー時にリトライを行いながら非同期操作を実行します(戻り値なし) + /// + /// 実行する操作 + /// 操作名(ログ用) + /// ロガー + /// キャンセルトークン + public static async Task ExecuteWithRetryAsync( + Func operation, + string operationName, + ILogger? logger = null, + CancellationToken cancellationToken = default) + { + var attempt = 0; + + while (true) + { + attempt++; + + try + { + await operation(); + return; + } + catch (Exception ex) when (IsRetryableError(ex)) + { + if (attempt >= MaxRetries) + { + logger?.LogError( + "{OperationName} failed after {MaxRetries} attempts. Last error: {ErrorMessage}", + operationName, MaxRetries, ex.Message); + throw; + } + + var retryDelay = GetRetryDelay(ex); + + logger?.LogWarning( + "{OperationName} attempt {Attempt}/{MaxRetries} failed with retryable error: {ErrorMessage}. " + + "Waiting {DelaySeconds} seconds before retry...", + operationName, attempt, MaxRetries, ex.Message, retryDelay.TotalSeconds); + + await Task.Delay(retryDelay, cancellationToken); + } + } + } + + /// + /// ストリーミングレスポンスを収集してリトライ処理を行います + /// ストリーミング中にエラーが発生した場合、最初からやり直します + /// + /// ストリーミング要素の型 + /// ストリーミング操作を作成する関数 + /// 操作名(ログ用) + /// ロガー + /// キャンセルトークン + /// 収集された結果のリスト + public static async Task> ExecuteStreamingWithRetryAsync( + Func> operationFactory, + string operationName, + ILogger? logger = null, + CancellationToken cancellationToken = default) + { + var attempt = 0; + + while (true) + { + attempt++; + var results = new List(); + + try + { + await foreach (var item in operationFactory()) + { + cancellationToken.ThrowIfCancellationRequested(); + results.Add(item); + } + + return results; + } + catch (Exception ex) when (IsRetryableError(ex)) + { + if (attempt >= MaxRetries) + { + logger?.LogError( + "{OperationName} streaming failed after {MaxRetries} attempts. Last error: {ErrorMessage}", + operationName, MaxRetries, ex.Message); + throw; + } + + var retryDelay = GetRetryDelay(ex); + + logger?.LogWarning( + "{OperationName} streaming attempt {Attempt}/{MaxRetries} failed with retryable error: {ErrorMessage}. " + + "Waiting {DelaySeconds} seconds before retry...", + operationName, attempt, MaxRetries, ex.Message, retryDelay.TotalSeconds); + + await Task.Delay(retryDelay, cancellationToken); + } + } + } + + /// + /// エラーがリトライ可能かどうかを判定します + /// + private static bool IsRetryableError(Exception ex) + { + // HTTP 429 (Too Many Requests) + if (ex is HttpRequestException httpEx) + { + if (httpEx.StatusCode == HttpStatusCode.TooManyRequests) + { + return true; + } + } + + // OpenAI API のレート制限エラー + var message = ex.Message.ToLowerInvariant(); + if (message.Contains("429") || + message.Contains("rate limit") || + message.Contains("too many requests") || + message.Contains("quota exceeded") || + message.Contains("requests per minute") || + message.Contains("tokens per minute")) + { + return true; + } + + // 内部例外をチェック + if (ex.InnerException != null) + { + return IsRetryableError(ex.InnerException); + } + + return false; + } + + /// + /// リトライ待機時間を取得します + /// + private static TimeSpan GetRetryDelay(Exception ex) + { + // Retry-Afterヘッダーから待機時間を取得しようとする + // デフォルトは1分 + return RetryDelay; + } +} From 1cb11a860ab6daadf971c9dd90bd2773c4bdaa7a Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:48:04 +0900 Subject: [PATCH 23/27] [fix] approve --- PRAgent/Services/GitHubService.cs | 98 +++++--------- PRAgent/Services/KernelService.cs | 24 ++-- PRAgent/Services/RetryHelper.cs | 206 ------------------------------ 3 files changed, 42 insertions(+), 286 deletions(-) delete mode 100644 PRAgent/Services/RetryHelper.cs diff --git a/PRAgent/Services/GitHubService.cs b/PRAgent/Services/GitHubService.cs index c3679f4..960017a 100644 --- a/PRAgent/Services/GitHubService.cs +++ b/PRAgent/Services/GitHubService.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Logging; using Octokit; using PRAgent.Models; @@ -7,31 +6,23 @@ namespace PRAgent.Services; public class GitHubService : IGitHubService { private readonly GitHubClient _client; - private readonly ILogger? _logger; - public GitHubService(string gitHubToken, ILogger? logger = null) + public GitHubService(string gitHubToken) { _client = new GitHubClient(new ProductHeaderValue("PRAgent")) { Credentials = new Credentials(gitHubToken) }; - _logger = logger; } public async Task GetPullRequestAsync(string owner, string repo, int prNumber) { - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Get(owner, repo, prNumber), - nameof(GetPullRequestAsync), - _logger); + return await _client.PullRequest.Get(owner, repo, prNumber); } public async Task> GetPullRequestFilesAsync(string owner, string repo, int prNumber) { - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Files(owner, repo, prNumber), - nameof(GetPullRequestFilesAsync), - _logger); + return await _client.PullRequest.Files(owner, repo, prNumber); } public async Task> GetPullRequestCommentsAsync(string owner, string repo, int prNumber) @@ -43,10 +34,7 @@ public async Task> GetPullRequestComment public async Task> GetPullRequestReviewCommentsAsync(string owner, string repo, int prNumber) { - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.Issue.Comment.GetAllForIssue(owner, repo, prNumber), - nameof(GetPullRequestReviewCommentsAsync), - _logger); + return await _client.Issue.Comment.GetAllForIssue(owner, repo, prNumber); } public async Task GetPullRequestDiffAsync(string owner, string repo, int prNumber) @@ -87,10 +75,7 @@ public async Task CreateReviewCommentAsync(string owner, stri Event = PullRequestReviewEvent.Comment }; - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment), - nameof(CreateReviewCommentAsync), - _logger); + return await _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment); } public async Task CreateReviewWithCommentsAsync(string owner, string repo, int prNumber, string reviewBody, List comments) @@ -102,10 +87,7 @@ public async Task CreateReviewWithCommentsAsync(string owner, Comments = comments }; - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment), - nameof(CreateReviewWithCommentsAsync), - _logger); + return await _client.PullRequest.Review.Create(owner, repo, prNumber, reviewComment); } /// @@ -120,18 +102,12 @@ public async Task CreateCompleteReviewAsync(string owner, string repo, int prNum Comments = comments }; - await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), - nameof(CreateCompleteReviewAsync), - _logger); + await _client.PullRequest.Review.Create(owner, repo, prNumber, review); } public async Task CreateIssueCommentAsync(string owner, string repo, int prNumber, string body) { - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.Issue.Comment.Create(owner, repo, prNumber, body), - nameof(CreateIssueCommentAsync), - _logger); + return await _client.Issue.Comment.Create(owner, repo, prNumber, body); } public async Task ApprovePullRequestAsync(string owner, string repo, int prNumber, string? comment = null) @@ -142,27 +118,17 @@ public async Task ApprovePullRequestAsync(string owner, strin Event = PullRequestReviewEvent.Approve }; - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), - nameof(ApprovePullRequestAsync), - _logger); + return await _client.PullRequest.Review.Create(owner, repo, prNumber, review); } public async Task GetRepositoryFileContentAsync(string owner, string repo, string path, string? branch = null) { try { - var defaultBranch = await RetryHelper.ExecuteWithRetryAsync( - () => _client.Repository.Get(owner, repo), - nameof(GetRepositoryFileContentAsync) + "_GetRepository", - _logger); - + var defaultBranch = await _client.Repository.Get(owner, repo); var reference = branch ?? $"heads/{defaultBranch.DefaultBranch}"; - var contents = await RetryHelper.ExecuteWithRetryAsync( - () => _client.Repository.Content.GetAllContentsByRef(owner, repo, path, reference), - nameof(GetRepositoryFileContentAsync) + "_GetContents", - _logger); + var contents = await _client.Repository.Content.GetAllContentsByRef(owner, repo, path, reference); if (contents.Count > 0) { @@ -192,19 +158,19 @@ public async Task CreateLineCommentAsync(string owner, string // 行コメントを作成 var commentBody = suggestion != null ? $"{comment}\n```suggestion\n{suggestion}\n```" : comment; - var review = new PullRequestReviewCreate - { - Event = PullRequestReviewEvent.Comment, - Comments = new List + return await _client.PullRequest.Review.Create( + owner, + repo, + prNumber, + new PullRequestReviewCreate { - new DraftPullRequestReviewComment(commentBody, filePath, lineNumber) + Event = PullRequestReviewEvent.Comment, + Comments = new List + { + new DraftPullRequestReviewComment(commentBody, filePath, lineNumber) + } } - }; - - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), - nameof(CreateLineCommentAsync), - _logger); + ); } public async Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)> comments) @@ -229,15 +195,15 @@ public async Task CreateMultipleLineCommentsAsync(string owne } }).ToList(); - var review = new PullRequestReviewCreate - { - Event = PullRequestReviewEvent.Comment, - Comments = draftComments - }; - - return await RetryHelper.ExecuteWithRetryAsync( - () => _client.PullRequest.Review.Create(owner, repo, prNumber, review), - nameof(CreateMultipleLineCommentsAsync), - _logger); + return await _client.PullRequest.Review.Create( + owner, + repo, + prNumber, + new PullRequestReviewCreate + { + Event = PullRequestReviewEvent.Comment, + Comments = draftComments + } + ); } } diff --git a/PRAgent/Services/KernelService.cs b/PRAgent/Services/KernelService.cs index d848246..9803cb0 100644 --- a/PRAgent/Services/KernelService.cs +++ b/PRAgent/Services/KernelService.cs @@ -110,17 +110,17 @@ public async IAsyncEnumerable InvokePromptAsync( string prompt, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) { - // プロンプトを出力 + // プロンプットを出力 _logger?.LogInformation("=== KernelService Prompt ===\n{Prompt}", prompt); var service = kernel.GetRequiredService(); var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(prompt); - // ストリーミングを実行(リトライなし) await foreach (var content in service.GetStreamingChatMessageContentsAsync(chatHistory, cancellationToken: cancellationToken)) { - yield return content.Content ?? string.Empty; + var responseContent = content.Content ?? string.Empty; + yield return responseContent; } _logger?.LogInformation("=== KernelService Response (Streaming) ===\n{Response}", ""); @@ -131,21 +131,17 @@ public async Task InvokePromptAsStringAsync( string prompt, CancellationToken cancellationToken = default) { - // プロンプトを出力 + // プロンプットを出力 _logger?.LogInformation("=== KernelService Prompt ===\n{Prompt}", prompt); - var service = kernel.GetRequiredService(); - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage(prompt); + var resultBuilder = new System.Text.StringBuilder(); - // リトライ付きでストリーミングを実行して結果を収集 - var results = await RetryHelper.ExecuteStreamingWithRetryAsync( - () => service.GetStreamingChatMessageContentsAsync(chatHistory, cancellationToken: cancellationToken), - "InvokePromptAsync", - _logger, - cancellationToken); + await foreach (var content in InvokePromptAsync(kernel, prompt, cancellationToken)) + { + resultBuilder.Append(content); + } - var response = string.Join("", results.Select(c => c.Content ?? string.Empty)); + var response = resultBuilder.ToString(); _logger?.LogInformation("=== KernelService Response ===\n{Response}", response); return response; diff --git a/PRAgent/Services/RetryHelper.cs b/PRAgent/Services/RetryHelper.cs deleted file mode 100644 index d6d16ad..0000000 --- a/PRAgent/Services/RetryHelper.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System.Net; -using Microsoft.Extensions.Logging; - -namespace PRAgent.Services; - -/// -/// HTTP 429エラー時のリトライ処理を提供するヘルパークラス -/// -public static class RetryHelper -{ - private const int MaxRetries = 30; - private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); - - /// - /// 429エラー時にリトライを行いながら非同期操作を実行します - /// - /// 戻り値の型 - /// 実行する操作 - /// 操作名(ログ用) - /// ロガー - /// キャンセルトークン - /// 操作の結果 - public static async Task ExecuteWithRetryAsync( - Func> operation, - string operationName, - ILogger? logger = null, - CancellationToken cancellationToken = default) - { - var attempt = 0; - - while (true) - { - attempt++; - - try - { - return await operation(); - } - catch (Exception ex) when (IsRetryableError(ex)) - { - if (attempt >= MaxRetries) - { - logger?.LogError( - "{OperationName} failed after {MaxRetries} attempts. Last error: {ErrorMessage}", - operationName, MaxRetries, ex.Message); - throw; - } - - var retryDelay = GetRetryDelay(ex); - - logger?.LogWarning( - "{OperationName} attempt {Attempt}/{MaxRetries} failed with retryable error: {ErrorMessage}. " + - "Waiting {DelaySeconds} seconds before retry...", - operationName, attempt, MaxRetries, ex.Message, retryDelay.TotalSeconds); - - await Task.Delay(retryDelay, cancellationToken); - } - } - } - - /// - /// 429エラー時にリトライを行いながら非同期操作を実行します(戻り値なし) - /// - /// 実行する操作 - /// 操作名(ログ用) - /// ロガー - /// キャンセルトークン - public static async Task ExecuteWithRetryAsync( - Func operation, - string operationName, - ILogger? logger = null, - CancellationToken cancellationToken = default) - { - var attempt = 0; - - while (true) - { - attempt++; - - try - { - await operation(); - return; - } - catch (Exception ex) when (IsRetryableError(ex)) - { - if (attempt >= MaxRetries) - { - logger?.LogError( - "{OperationName} failed after {MaxRetries} attempts. Last error: {ErrorMessage}", - operationName, MaxRetries, ex.Message); - throw; - } - - var retryDelay = GetRetryDelay(ex); - - logger?.LogWarning( - "{OperationName} attempt {Attempt}/{MaxRetries} failed with retryable error: {ErrorMessage}. " + - "Waiting {DelaySeconds} seconds before retry...", - operationName, attempt, MaxRetries, ex.Message, retryDelay.TotalSeconds); - - await Task.Delay(retryDelay, cancellationToken); - } - } - } - - /// - /// ストリーミングレスポンスを収集してリトライ処理を行います - /// ストリーミング中にエラーが発生した場合、最初からやり直します - /// - /// ストリーミング要素の型 - /// ストリーミング操作を作成する関数 - /// 操作名(ログ用) - /// ロガー - /// キャンセルトークン - /// 収集された結果のリスト - public static async Task> ExecuteStreamingWithRetryAsync( - Func> operationFactory, - string operationName, - ILogger? logger = null, - CancellationToken cancellationToken = default) - { - var attempt = 0; - - while (true) - { - attempt++; - var results = new List(); - - try - { - await foreach (var item in operationFactory()) - { - cancellationToken.ThrowIfCancellationRequested(); - results.Add(item); - } - - return results; - } - catch (Exception ex) when (IsRetryableError(ex)) - { - if (attempt >= MaxRetries) - { - logger?.LogError( - "{OperationName} streaming failed after {MaxRetries} attempts. Last error: {ErrorMessage}", - operationName, MaxRetries, ex.Message); - throw; - } - - var retryDelay = GetRetryDelay(ex); - - logger?.LogWarning( - "{OperationName} streaming attempt {Attempt}/{MaxRetries} failed with retryable error: {ErrorMessage}. " + - "Waiting {DelaySeconds} seconds before retry...", - operationName, attempt, MaxRetries, ex.Message, retryDelay.TotalSeconds); - - await Task.Delay(retryDelay, cancellationToken); - } - } - } - - /// - /// エラーがリトライ可能かどうかを判定します - /// - private static bool IsRetryableError(Exception ex) - { - // HTTP 429 (Too Many Requests) - if (ex is HttpRequestException httpEx) - { - if (httpEx.StatusCode == HttpStatusCode.TooManyRequests) - { - return true; - } - } - - // OpenAI API のレート制限エラー - var message = ex.Message.ToLowerInvariant(); - if (message.Contains("429") || - message.Contains("rate limit") || - message.Contains("too many requests") || - message.Contains("quota exceeded") || - message.Contains("requests per minute") || - message.Contains("tokens per minute")) - { - return true; - } - - // 内部例外をチェック - if (ex.InnerException != null) - { - return IsRetryableError(ex.InnerException); - } - - return false; - } - - /// - /// リトライ待機時間を取得します - /// - private static TimeSpan GetRetryDelay(Exception ex) - { - // Retry-Afterヘッダーから待機時間を取得しようとする - // デフォルトは1分 - return RetryDelay; - } -} From 487e5c2d84493282c93bd26c80e10fcfc26c2af7 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:10:38 +0900 Subject: [PATCH 24/27] [fix] diff comment --- PRAgent.Tests/DiffPositionCalculatorTests.cs | 225 +++++++++++++++++++ PRAgent/Services/GitHubService.cs | 122 +++++++++- 2 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 PRAgent.Tests/DiffPositionCalculatorTests.cs 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/Services/GitHubService.cs b/PRAgent/Services/GitHubService.cs index 960017a..55a1a91 100644 --- a/PRAgent/Services/GitHubService.cs +++ b/PRAgent/Services/GitHubService.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Octokit; using PRAgent.Models; @@ -15,6 +16,74 @@ public GitHubService(string gitHubToken) }; } + /// + /// ファイルのdiffから行番号に対応するdiff positionを計算します + /// + /// ファイルのdiffパッチ + /// ファイル内の行番号(1ベース) + /// diff position(1ベース)、見つからない場合はnull + 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を解析: @@ -start_old,count +start_new,count @@ heading + var hunkMatch = 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; + } + } + // \ No newline at end of file などのメタ行はスキップ + } + + return null; + } + + /// + /// PRのファイルのdiffを取得します + /// + private async Task GetFilePatchAsync(string owner, string repo, int prNumber, string filePath) + { + var files = await _client.PullRequest.Files(owner, repo, prNumber); + var file = files.FirstOrDefault(f => f.FileName == filePath); + return file?.Patch; + } + public async Task GetPullRequestAsync(string owner, string repo, int prNumber) { return await _client.PullRequest.Get(owner, repo, prNumber); @@ -158,6 +227,15 @@ public async Task CreateLineCommentAsync(string owner, string // 行コメントを作成 var commentBody = suggestion != null ? $"{comment}\n```suggestion\n{suggestion}\n```" : comment; + // diffを取得してpositionを計算 + var patch = await GetFilePatchAsync(owner, repo, prNumber, filePath); + var position = CalculateDiffPosition(patch, lineNumber); + + if (!position.HasValue) + { + throw new ArgumentException($"Could not find line {lineNumber} in diff for file {filePath}. The line may not be part of the changes."); + } + return await _client.PullRequest.Review.Create( owner, repo, @@ -167,7 +245,7 @@ public async Task CreateLineCommentAsync(string owner, string Event = PullRequestReviewEvent.Comment, Comments = new List { - new DraftPullRequestReviewComment(commentBody, filePath, lineNumber) + new DraftPullRequestReviewComment(commentBody, filePath, position.Value) } } ); @@ -175,25 +253,53 @@ public async Task CreateLineCommentAsync(string owner, string public async Task CreateMultipleLineCommentsAsync(string owner, string repo, int prNumber, List<(string FilePath, int? LineNumber, int? StartLine, int? EndLine, string Comment, string? Suggestion)> comments) { - var draftComments = comments.Select(c => + // ファイルごとのdiffをキャッシュ + var patchCache = new Dictionary(); + + var draftComments = new List(); + var errors = new List(); + + foreach (var c in comments) { var commentBody = c.Suggestion != null ? $"{c.Comment}\n```suggestion\n{c.Suggestion}\n```" : c.Comment; - // 1行コメントのみ対応(LineNumberがある場合) + int targetLine; if (c.LineNumber.HasValue) { - return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.LineNumber.Value); + targetLine = c.LineNumber.Value; } - // 範囲コメントは1行目を使用 else if (c.StartLine.HasValue) { - return new DraftPullRequestReviewComment(commentBody, c.FilePath, c.StartLine.Value); + targetLine = c.StartLine.Value; } else { - throw new ArgumentException($"Comment must have either LineNumber or StartLine: {c.FilePath}"); + errors.Add($"Comment must have either LineNumber or StartLine: {c.FilePath}"); + continue; } - }).ToList(); + + // diffを取得(キャッシュを使用) + if (!patchCache.TryGetValue(c.FilePath, out var patch)) + { + patch = await GetFilePatchAsync(owner, repo, prNumber, c.FilePath); + patchCache[c.FilePath] = patch; + } + + // positionを計算 + var position = CalculateDiffPosition(patch, targetLine); + if (!position.HasValue) + { + errors.Add($"Could not find line {targetLine} in diff for file {c.FilePath}"); + continue; + } + + draftComments.Add(new DraftPullRequestReviewComment(commentBody, c.FilePath, position.Value)); + } + + if (errors.Count > 0 && draftComments.Count == 0) + { + throw new ArgumentException($"Failed to create any comments: {string.Join("; ", errors)}"); + } return await _client.PullRequest.Review.Create( owner, From 00b721d1371d7b6fe143b69bef07baa3368d89ac Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:19:32 +0900 Subject: [PATCH 25/27] [fix] comments --- PRAgent/Plugins/GitHub/PostCommentFunction.cs | 7 +- PRAgent/Services/GitHubService.cs | 21 +- PRAgent/Services/IGitHubService.cs | 11 + PRAgent/Services/KernelService.cs | 28 +-- PRAgent/Services/PRActionExecutor.cs | 207 ++++++++++++++---- 5 files changed, 193 insertions(+), 81 deletions(-) diff --git a/PRAgent/Plugins/GitHub/PostCommentFunction.cs b/PRAgent/Plugins/GitHub/PostCommentFunction.cs index 784ccea..04f89f0 100644 --- a/PRAgent/Plugins/GitHub/PostCommentFunction.cs +++ b/PRAgent/Plugins/GitHub/PostCommentFunction.cs @@ -36,7 +36,7 @@ public string PostCommentAsync(string comment) /// プルリクエストの特定の行にコメントを追加します(バッファに追加) /// /// ファイルパス - /// 行番号 + /// 行番号(1以上) /// コメント内容 /// 提案される変更内容(オプション) /// 行コメントがバッファに追加されたことを示すメッセージ @@ -52,6 +52,11 @@ public string PostLineCommentAsync( return "Error: File path cannot be empty"; } + if (lineNumber <= 0) + { + return "Error: Line number must be a positive integer (1 or greater)"; + } + if (string.IsNullOrWhiteSpace(comment)) { return "Error: Comment cannot be empty"; diff --git a/PRAgent/Services/GitHubService.cs b/PRAgent/Services/GitHubService.cs index 55a1a91..2b7d4fc 100644 --- a/PRAgent/Services/GitHubService.cs +++ b/PRAgent/Services/GitHubService.cs @@ -59,9 +59,10 @@ public GitHubService(string gitHubToken) // 削除行: 新しいファイルの行番号は変わらない // 削除行にはコメントできないのでスキップ } - else if (line.StartsWith(" ") || line == "") + else if (line.StartsWith(" ") || (line.Length == 0 && position < lines.Length)) { - // コンテキスト行または空行 + // コンテキスト行 + // 空行はdiffの最後のアーティファクト(Splitの結果)を除く currentNewLine++; if (currentNewLine == lineNumber) { @@ -160,18 +161,24 @@ public async Task CreateReviewWithCommentsAsync(string owner, } /// - /// レビュー本文と詳細コメントをまとめて投稿 + /// レビュー本文、行コメント、承認ステータスをまとめて1つのReviewとして投稿します /// - public async Task CreateCompleteReviewAsync(string owner, string repo, int prNumber, string reviewBody, List comments) + public async Task CreateCompleteReviewAsync( + string owner, + string repo, + int prNumber, + string? reviewBody, + List comments, + bool approve = false) { var review = new PullRequestReviewCreate() { - Body = reviewBody, - Event = PullRequestReviewEvent.Comment, + Body = reviewBody ?? string.Empty, + Event = approve ? PullRequestReviewEvent.Approve : PullRequestReviewEvent.Comment, Comments = comments }; - await _client.PullRequest.Review.Create(owner, repo, prNumber, review); + return await _client.PullRequest.Review.Create(owner, repo, prNumber, review); } public async Task CreateIssueCommentAsync(string owner, string repo, int prNumber, string body) diff --git a/PRAgent/Services/IGitHubService.cs b/PRAgent/Services/IGitHubService.cs index 5722ac1..0007d66 100644 --- a/PRAgent/Services/IGitHubService.cs +++ b/PRAgent/Services/IGitHubService.cs @@ -16,4 +16,15 @@ public interface IGitHubService Task ApprovePullRequestAsync(string owner, string repo, int prNumber, string? comment = null); Task GetRepositoryFileContentAsync(string owner, string repo, string path, string? branch = null); Task FileExistsAsync(string owner, string repo, string path, string? branch = null); + + /// + /// レビュー本文、行コメント、承認ステータスをまとめて1つのReviewとして投稿します + /// + Task CreateCompleteReviewAsync( + string owner, + string repo, + int prNumber, + string? reviewBody, + List comments, + bool approve = false); } diff --git a/PRAgent/Services/KernelService.cs b/PRAgent/Services/KernelService.cs index 9803cb0..7257189 100644 --- a/PRAgent/Services/KernelService.cs +++ b/PRAgent/Services/KernelService.cs @@ -53,32 +53,8 @@ public Kernel CreateKernel(string? systemPrompt = null) public Kernel CreateAgentKernel(string? systemPrompt = null) { - var builder = Kernel.CreateBuilder(); - - var endpoint = _aiSettings.Endpoint; - - // エンドポイントが指定されている場合はカスタムエンドポイントを使用 - if (!string.IsNullOrEmpty(endpoint)) - { - _logger?.LogInformation("Using custom endpoint: {Endpoint}", endpoint); - builder.Services.AddOpenAIChatCompletion( - modelId: _aiSettings.ModelId, - apiKey: _aiSettings.ApiKey, - endpoint: new Uri(endpoint) - ); - } - else - { - _logger?.LogInformation("Using default OpenAI endpoint"); - builder.AddOpenAIChatCompletion( - modelId: _aiSettings.ModelId, - apiKey: _aiSettings.ApiKey - ); - } - - var kernel = builder.Build(); - - return kernel; + // CreateKernelと同じ実装なので委譲 + return CreateKernel(systemPrompt); } public Kernel RegisterFunctionPlugins(Kernel kernel, IEnumerable plugins) diff --git a/PRAgent/Services/PRActionExecutor.cs b/PRAgent/Services/PRActionExecutor.cs index ad011ee..132cea1 100644 --- a/PRAgent/Services/PRActionExecutor.cs +++ b/PRAgent/Services/PRActionExecutor.cs @@ -40,36 +40,109 @@ public async Task ExecuteAsync(PRActionBuffer buffer, Cancellati try { - // 1. レビューコメントを投稿 + // ファイルごとのdiffをキャッシュ + var patchCache = new Dictionary(); + var draftComments = new List(); + var errors = new List(); + + // 行コメントを処理してDraftPullRequestReviewCommentに変換 + foreach (var lineComment in buffer.LineComments) + { + int targetLine; + if (lineComment.LineNumber.HasValue) + { + targetLine = lineComment.LineNumber.Value; + } + else if (lineComment.StartLine.HasValue) + { + targetLine = lineComment.StartLine.Value; + } + else + { + errors.Add($"Line comment must have either LineNumber or StartLine: {lineComment.FilePath}"); + continue; + } + + var commentBody = lineComment.Suggestion != null + ? $"{lineComment.Comment}\n```suggestion\n{lineComment.Suggestion}\n```" + : lineComment.Comment; + + // diffを取得(キャッシュを使用) + if (!patchCache.TryGetValue(lineComment.FilePath, out var patch)) + { + patch = await GetFilePatchAsync(lineComment.FilePath); + patchCache[lineComment.FilePath] = patch; + } + + // positionを計算 + var position = CalculateDiffPosition(patch, targetLine); + if (!position.HasValue) + { + errors.Add($"Could not find line {targetLine} in diff for file {lineComment.FilePath}"); + continue; + } + + draftComments.Add(new DraftPullRequestReviewComment(commentBody, lineComment.FilePath, position.Value)); + } + + // レビュー本文を作成 + var reviewBodyBuilder = new System.Text.StringBuilder(); + + // レビューコメントを追加 if (buffer.ReviewComments.Count > 0) { foreach (var reviewComment in buffer.ReviewComments) { - await _gitHubService.CreateReviewCommentAsync( - _owner, _repo, _prNumber, reviewComment.Comment); + reviewBodyBuilder.AppendLine(reviewComment.Comment); + reviewBodyBuilder.AppendLine(); } + } + + // 承認コメントを追加 + if (!string.IsNullOrEmpty(buffer.ApprovalComment)) + { + reviewBodyBuilder.AppendLine(buffer.ApprovalComment); + } + + var reviewBody = reviewBodyBuilder.ToString().Trim(); + + // 1つのReviewとしてまとめて投稿 + bool hasComments = draftComments.Count > 0 || !string.IsNullOrEmpty(reviewBody); + bool shouldApprove = buffer.ApprovalState == PRApprovalState.Approved; + + if (hasComments || shouldApprove) + { + var reviewResult = await _gitHubService.CreateCompleteReviewAsync( + _owner, + _repo, + _prNumber, + reviewBody, + draftComments, + shouldApprove); + result.ReviewCommentsPosted = buffer.ReviewComments.Count; + result.LineCommentsPosted = draftComments.Count; + + if (shouldApprove) + { + result.Approved = true; + result.ApprovalState = PRApprovalState.Approved; + result.ApprovalUrl = reviewResult.HtmlUrl; + } } - // 2. 行コメントを投稿 - if (buffer.LineComments.Count > 0) + // 変更依頼の場合は別途投稿 + if (buffer.ApprovalState == PRApprovalState.ChangesRequested) { - var comments = buffer.LineComments.Select(c => ( - c.FilePath, - c.LineNumber, - c.StartLine, - c.EndLine, - c.Comment, - c.Suggestion - )).ToList(); - - var reviewResult = await _gitHubService.CreateMultipleLineCommentsAsync( - _owner, _repo, _prNumber, comments); - - result.LineCommentsPosted = comments.Count; + var changesComment = $"## Changes Requested\n\n{buffer.ApprovalComment ?? "Please address the issues mentioned in the review."}"; + await _gitHubService.CreateReviewCommentAsync( + _owner, _repo, _prNumber, changesComment); + + result.ApprovalState = PRApprovalState.ChangesRequested; + result.ChangesRequested = true; } - // 3. サマリーを全体コメントとして投稿 + // サマリーを全体コメントとして投稿 if (buffer.Summaries.Count > 0) { var summaryText = string.Join("\n\n", buffer.Summaries); @@ -83,7 +156,7 @@ await _gitHubService.CreateReviewCommentAsync( result.SummaryCommentUrl = commentResult.HtmlUrl; } - // 4. 全体コメントを投稿 + // 全体コメントを投稿 if (!string.IsNullOrEmpty(buffer.GeneralComment)) { var commentResult = await _gitHubService.CreateIssueCommentAsync( @@ -93,33 +166,6 @@ await _gitHubService.CreateReviewCommentAsync( result.GeneralCommentUrl = commentResult.HtmlUrl; } - // 5. 承認ステータスに応じた処理を実行 - switch (buffer.ApprovalState) - { - case PRApprovalState.Approved: - var approvalResult = await _gitHubService.ApprovePullRequestAsync( - _owner, _repo, _prNumber, buffer.ApprovalComment); - - result.Approved = true; - result.ApprovalState = PRApprovalState.Approved; - result.ApprovalUrl = approvalResult.HtmlUrl; - break; - - case PRApprovalState.ChangesRequested: - // 変更依頼をレビューコメントとして投稿 - var changesComment = $"## Changes Requested\n\n{buffer.ApprovalComment ?? "Please address the issues mentioned in the review."}"; - await _gitHubService.CreateReviewCommentAsync( - _owner, _repo, _prNumber, changesComment); - - result.ApprovalState = PRApprovalState.ChangesRequested; - result.ChangesRequested = true; - break; - - case PRApprovalState.None: - // 何もしない(コメントのみ) - break; - } - result.TotalActionsPosted = result.ReviewCommentsPosted + result.LineCommentsPosted + @@ -129,6 +175,11 @@ await _gitHubService.CreateReviewCommentAsync( (result.ChangesRequested ? 1 : 0); result.Message = $"Successfully posted {result.TotalActionsPosted} action(s) to PR #{_prNumber}"; + + if (errors.Count > 0) + { + result.Message += $"\n\nWarnings: {string.Join("; ", errors)}"; + } } catch (Exception ex) { @@ -140,6 +191,68 @@ await _gitHubService.CreateReviewCommentAsync( return result; } + /// + /// ファイルのdiffを取得します + /// + private async Task GetFilePatchAsync(string filePath) + { + var files = await _gitHubService.GetPullRequestFilesAsync(_owner, _repo, _prNumber); + var file = files.FirstOrDefault(f => f.FileName == filePath); + return file?.Patch; + } + + /// + /// ファイルのdiffから行番号に対応するdiff positionを計算します + /// + 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を解析: @@ -start_old,count +start_new,count @@ heading + 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.Length == 0 && position < lines.Length)) + { + // コンテキスト行(空行はdiffの最後のアーティファクトを除く) + currentNewLine++; + if (currentNewLine == lineNumber) + { + return position; + } + } + } + + return null; + } + /// /// アクションをGitHubに投稿する前に確認するためのサマリーを作成します /// From faeaec3155c1626935f351801ae2e4f3194e2033 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:14:26 +0900 Subject: [PATCH 26/27] [fix] agents --- PRAgent/Agents/AgentDefinition.cs | 47 +-- PRAgent/Agents/AgentFactory.cs | 95 +---- PRAgent/Agents/DetailedCommentAgent.cs | 270 ++++++------- PRAgent/Agents/SK/SKApprovalAgent.cs | 369 ------------------ PRAgent/Agents/SK/SKReviewAgent.cs | 198 +++++----- PRAgent/Agents/SK/SKSummaryAgent.cs | 103 ----- .../ServiceCollectionExtensions.cs | 2 - .../Plugins/Agent/AgentInvocationFunctions.cs | 179 +-------- PRAgent/Program.cs | 2 - PRAgent/Services/IAgentOrchestratorService.cs | 15 +- PRAgent/Services/IDetailedCommentAgent.cs | 20 +- PRAgent/Services/PRAnalysisService.cs | 10 +- .../Services/SK/SKAgentOrchestratorService.cs | 289 ++++---------- 13 files changed, 317 insertions(+), 1282 deletions(-) delete mode 100644 PRAgent/Agents/SK/SKApprovalAgent.cs delete mode 100644 PRAgent/Agents/SK/SKSummaryAgent.cs diff --git a/PRAgent/Agents/AgentDefinition.cs b/PRAgent/Agents/AgentDefinition.cs index af16b7f..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: """ - あなたはプルリクエストの承認決定を行うシニアテクニカルリードです。 - - あなたの役割: - 1. コードレビュー結果を分析 - 2. 承認基準に照らして評価 - 3. 保守的でリスクを考慮した承認決定を行う - 4. 判断について明確な理由を提供 - - 承認基準: - - critical: 重大な問題が0件であること - - major: 重大または重大な問題が0件であること - - minor: 軽微、重大、重大な問題が0件であること - - none: 常に承認 - - 不確実な場合は、慎重を期して追加レビューまたは変更依頼を推奨します。 - """, - 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 index b3b57f2..573a1d1 100644 --- a/PRAgent/Agents/AgentFactory.cs +++ b/PRAgent/Agents/AgentFactory.cs @@ -1,6 +1,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; using PRAgent.Services; namespace PRAgent.Agents; @@ -56,89 +57,29 @@ public async Task CreateReviewAgentAsync( } /// - /// Summaryエージェントを作成 + /// Reviewエージェント用のKernelを作成(FunctionCalling有効) /// - public async Task CreateSummaryAgentAsync( + public Kernel CreateReviewKernel( string owner, string repo, int prNumber, - string? customSystemPrompt = null, - IEnumerable? functions = null) - { - var kernel = _kernelService.CreateAgentKernel(AgentDefinition.SummaryAgent.SystemPrompt); - - if (functions != null) - { - foreach (var function in functions) - { - kernel.ImportPluginFromObject(function); - } - } - - var agent = new ChatCompletionAgent - { - Name = AgentDefinition.SummaryAgent.Name, - Description = AgentDefinition.SummaryAgent.Description, - Instructions = customSystemPrompt ?? AgentDefinition.SummaryAgent.SystemPrompt, - Kernel = kernel - }; - - return await Task.FromResult(agent); - } - - /// - /// Approvalエージェントを作成 - /// - public async Task CreateApprovalAgentAsync( - string owner, - string repo, - int prNumber, - string? customSystemPrompt = null, - IEnumerable? functions = null) + string? customSystemPrompt = null) { - var kernel = CreateApprovalKernel(owner, repo, prNumber, customSystemPrompt); - - // GitHub操作用のプラグインを登録 - if (functions != null) - { - foreach (var function in functions) - { - kernel.ImportPluginFromObject(function); - } - } - - var agent = new ChatCompletionAgent - { - Name = AgentDefinition.ApprovalAgent.Name, - Description = AgentDefinition.ApprovalAgent.Description, - Instructions = customSystemPrompt ?? AgentDefinition.ApprovalAgent.SystemPrompt, - Kernel = kernel, - Arguments = new KernelArguments - { - // Approvalエージェント用の特殊設定 - ["approval_mode"] = true, - ["owner"] = owner, - ["repo"] = repo, - ["pr_number"] = prNumber, - // FunctionCallingを自動的に有効にする - ["function_choice_behavior"] = "auto" - } - }; - - return await Task.FromResult(agent); + return _kernelService.CreateAgentKernel( + customSystemPrompt ?? AgentDefinition.ReviewAgent.SystemPrompt); } /// - /// Approvalエージェント用のKernelを作成 + /// 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 _kernelService.CreateAgentKernel( - customSystemPrompt ?? AgentDefinition.ApprovalAgent.SystemPrompt); + return CreateReviewKernel(owner, repo, prNumber, customSystemPrompt); } /// @@ -175,22 +116,4 @@ public async Task CreateCustomAgentAsync( return await Task.FromResult(agent); } - - /// - /// 複数のエージェントを一度に作成 - /// - public async Task<(ChatCompletionAgent reviewAgent, ChatCompletionAgent summaryAgent, ChatCompletionAgent approvalAgent)> CreateAllAgentsAsync( - string owner, - string repo, - int prNumber, - string? customReviewPrompt = null, - string? customSummaryPrompt = null, - string? customApprovalPrompt = null) - { - var reviewAgent = await CreateReviewAgentAsync(owner, repo, prNumber, customReviewPrompt); - var summaryAgent = await CreateSummaryAgentAsync(owner, repo, prNumber, customSummaryPrompt); - var approvalAgent = await CreateApprovalAgentAsync(owner, repo, prNumber, customApprovalPrompt); - - return (reviewAgent, summaryAgent, approvalAgent); - } } diff --git a/PRAgent/Agents/DetailedCommentAgent.cs b/PRAgent/Agents/DetailedCommentAgent.cs index 208bff0..f0b5e91 100644 --- a/PRAgent/Agents/DetailedCommentAgent.cs +++ b/PRAgent/Agents/DetailedCommentAgent.cs @@ -1,6 +1,6 @@ -using Microsoft.SemanticKernel; using Microsoft.Extensions.Logging; -using Octokit; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; using PRAgent.Models; using PRAgent.Services; @@ -8,203 +8,155 @@ namespace PRAgent.Agents; /// /// 詳細な行コメントを作成するサブエージェント -/// Tool呼び出しベースで各問題点ごとにコメントを作成 +/// ReviewAgentが作成した概要から、具体的な行コメントを生成 /// -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; + } /// - /// レビュー結果から詳細な行コメントを作成 + /// レビュー概要から詳細な行コメントを作成 /// - public async Task> CreateCommentsAsync(string review, string language) + public async Task> CreateCommentsAsync(string reviewOverview, string language) { SetLanguage(language); - // レビュー内容から問題点を抽出 - var issues = ExtractIssuesFromReview(review); - - // 各問題点に対してTool呼び出しでコメントを作成 - var comments = new List(); - - foreach (var issue in issues) - { - var comment = await CreateCommentForIssueAsync(issue, language); - if (comment != null) - { - comments.Add(comment); - } - } - - return comments; - } + 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配列のみを出力してください。 + """; - /// - /// レビューから問題点を抽出 - /// - private List ExtractIssuesFromReview(string review) - { - var issues = new List(); + var chatService = kernel.GetRequiredService(); + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage(prompt); - // セクションごとに分割 - var sections = review.Split(new[] { "\n\n### ", "\n## ", "\n##\n\n" }, StringSplitOptions.RemoveEmptyEntries); + var response = await chatService.GetChatMessageContentsAsync(chatHistory, executionSettings: null, kernel); + var responseText = response.FirstOrDefault()?.Content ?? "[]"; - foreach (var section in sections) - { - // セクションタイトルを解析 - var titleMatch = System.Text.RegularExpressions.Regex.Match(section, @"^(###\s*\[([A-Z]+)\])?\s*(.+)"); - if (titleMatch.Success) - { - var levelStr = titleMatch.Groups[2].Value; - var title = titleMatch.Groups[3].Value.Trim(); - - // レベルを判定 - var level = levelStr switch - { - "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); - } - } - - // 問題説明を抽出 - 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(); - - // 修正提案を抽出 - var suggestion = ""; - var suggestionMatch = System.Text.RegularExpressions.Regex.Match(section, @"```suggestion\s*\n?([^\n`]+)"); - if (suggestionMatch.Success) - { - suggestion = suggestionMatch.Groups[1].Value.Trim(); - } - - issues.Add(new ReviewIssue - { - Title = title, - Level = level, - FilePath = path, - StartLine = startLine ?? 1, - EndLine = endLine ?? 1, - Description = problem, - Suggestion = suggestion - }); - } - } + _logger.LogInformation("=== DetailedCommentAgent Response ===\n{Response}", responseText); - return issues; + // JSONをパース + return ParseCommentsFromJson(responseText); } /// - /// 各問題点に対してコメントを作成 + /// JSONから行コメントデータをパース /// - private async Task CreateCommentForIssueAsync(ReviewIssue issue, string language) + private List ParseCommentsFromJson(string json) { try { - var prompt = CreateCommentPrompt(issue, language); - - // プロンプットを出力 - _logger.LogInformation("=== DetailedCommentAgent Prompt for Issue ===\n{Prompt}", prompt); + // まずコードブロック内の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(aiResponse, issue.FilePath, issue.StartLine); + return comments ?? new List(); } - catch + catch (Exception ex) { - // フォールバック:簡潔なコメントを作成 - return new DraftPullRequestReviewComment( - $"{issue.Level}: {issue.Description}\n\n{issue.Suggestion}", - issue.FilePath, - 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のプルリクエストレビュー用の詳細コメントを作成する専門家です。 + + ## 役割 + レビュー概要から、具体的な行コメントを抽出・生成 + + ## 出力ルール + 1. 有効なJSON配列のみを出力 + 2. 各コメントには以下を含める: + - path: ファイルパス + - line: 行番号 + - body: コメント本文 + - suggestion: 修正提案(オプション) + 3. コメントは簡潔かつ建設的に + """; + } + else + { + return """ + You are an expert at creating detailed line comments for GitHub pull request reviews. + + ## Your Role + Extract and generate specific line comments from review overview. + + ## Output Rules + 1. Output only valid JSON array + 2. Each comment should include: + - path: file path + - line: line number + - body: comment body + - suggestion: fix suggestion (optional) + 3. Keep comments concise and constructive + """; + } + } } - -/// -/// レベルの列挙型 -/// -public enum Severity -{ - Critical, - Major, - Minor, - Positive -} \ No newline at end of file diff --git a/PRAgent/Agents/SK/SKApprovalAgent.cs b/PRAgent/Agents/SK/SKApprovalAgent.cs deleted file mode 100644 index 2954c43..0000000 --- a/PRAgent/Agents/SK/SKApprovalAgent.cs +++ /dev/null @@ -1,369 +0,0 @@ -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ベースの承認エージェント -/// -public class SKApprovalAgent -{ - private readonly PRAgentFactory _agentFactory; - private readonly IGitHubService _gitHubService; - private readonly PullRequestDataService _prDataService; - - public SKApprovalAgent( - PRAgentFactory agentFactory, - IGitHubService gitHubService, - PullRequestDataService prDataService) - { - _agentFactory = agentFactory; - _gitHubService = gitHubService; - _prDataService = prDataService; - } - - /// - /// レビュー結果に基づいて承認決定を行います - /// - public async Task<(bool ShouldApprove, string Reasoning, string? Comment)> DecideAsync( - string owner, - string repo, - int prNumber, - string reviewResult, - ApprovalThreshold threshold, - CancellationToken cancellationToken = default) - { - // PR情報を取得 - 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 agent = await _agentFactory.CreateApprovalAgentAsync(owner, repo, prNumber); - - 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 ApprovalResponseParser.Parse(responses.ToString()); - } - - /// - /// プルリクエストを承認します - /// - 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}"; - } - - /// - /// レビューと承認を自動的に行います(承認条件を満たす場合) - /// - public async Task<(bool Approved, string Reasoning)> ReviewAndApproveAsync( - string owner, - string repo, - int prNumber, - ApprovalThreshold threshold, - CancellationToken cancellationToken = default) - { - // まずReviewエージェントを呼び出してレビューを実行 - var reviewAgent = new SKReviewAgent(_agentFactory, _prDataService, _gitHubService); - var reviewResult = await reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - - // 決定を行う - var (shouldApprove, reasoning, comment) = await DecideAsync( - owner, repo, prNumber, reviewResult, threshold, cancellationToken); - - // 承認する場合、実際に承認を実行 - if (shouldApprove) - { - await ApproveAsync(owner, repo, prNumber, comment); - return (true, $"Approved: {reasoning}"); - } - - return (false, $"Not approved: {reasoning}"); - } - - /// - /// バッファを使用してGitHub操作関数を持つApprovalエージェントを作成します - /// - public async Task CreateAgentWithBufferAsync( - string owner, - string repo, - int prNumber, - PRActionBuffer buffer, - string? customSystemPrompt = null, - string? language = null) - { - // プラグインインスタンスを作成 - var approvePlugin = new ApprovePRFunction(buffer); - var commentPlugin = new PostCommentFunction(buffer); - - // Kernelを作成してプラグインを登録 - var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, customSystemPrompt); - kernel.ImportPluginFromObject(approvePlugin); - kernel.ImportPluginFromObject(commentPlugin); - - // 言語に応じたシステムプロンプトを作成 - var systemPrompt = customSystemPrompt ?? GetSystemPrompt(language); - - // エージェントを作成 - var agent = new ChatCompletionAgent - { - Name = AgentDefinition.ApprovalAgent.Name, - Description = AgentDefinition.ApprovalAgent.Description, - Instructions = systemPrompt, - Kernel = kernel - }; - - return await Task.FromResult(agent); - } - - /// - /// 言語に応じたシステムプロンプトを取得します - /// - private static string GetSystemPrompt(string? language) - { - var isJapanese = language?.ToLowerInvariant() == "ja"; - - if (isJapanese) - { - return $""" - あなたはプルリクエストの承認決定を行うシニアテクニカルリードです。 - - あなたの役割: - 1. コードレビュー結果を分析 - 2. 承認基準に照らして評価 - 3. 保守的でリスクを考慮した承認決定を行う - 4. 判断について明確な理由を提供 - - ## 利用可能な関数 - 以下の関数を呼び出してアクションをバッファに追加してください: - - approve_pull_request - PRを承認 - - request_changes - 変更を依頼 - - post_pr_comment - 全体コメントを追加 - - post_line_comment - 特定行にコメントを追加 - - post_range_comment - 複数行にコメントを追加(開始行と終了行を指定) - - post_review_comment - レビューレベルのコメントを追加 - - ## 関数呼び出しの方法 - 関数を呼び出すには、関数名と必要なパラメータを明記してください: - 例: 「approve_pull_request関数を呼び出します。コメント: 良好です」 - - すべてのアクションはバッファに追加され、分析完了後に一括でGitHubに投稿されます。 - - ## 承認基準 - - critical: 重大な問題が0件であること - - major: 重大または重大な問題が0件であること - - minor: 軽微、重大、重大な問題が0件であること - - none: 常に承認 - - 不確実な場合は、慎重を期して追加レビューまたは変更依頼を推奨します。 - """; - } - else - { - return $""" - 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 - - ## Available Functions - Call the following functions to add actions to buffer: - - approve_pull_request - Approve the PR - - request_changes - Request changes - - post_pr_comment - Add a general comment - - post_line_comment - Add a comment to a specific line - - post_range_comment - Add a comment to a range of lines - - post_review_comment - Add a review-level comment - - ## How to Call Functions - Explicitly state that you are calling a function with its parameters: - Example: "I will call the approve_pull_request function with comment: Looks good." - - All actions will be buffered and posted to GitHub after your analysis is complete. - - ## 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. - """; - } - } - - /// - /// バッファリングパターンを使用して決定を行い、完了後にアクションを一括実行します - /// - public async Task<(bool ShouldApprove, string Reasoning, string? Comment, PRActionResult? ActionResult)> DecideWithFunctionCallingAsync( - string owner, - string repo, - int prNumber, - string reviewResult, - ApprovalThreshold threshold, - bool autoApprove = false, - string? language = null, - CancellationToken cancellationToken = default) - { - // バッファを作成 - var buffer = new PRActionBuffer(); - - // バッファを使用したエージェントを作成(言語指定) - var agent = await CreateAgentWithBufferAsync(owner, repo, prNumber, buffer, language: language); - - // PR情報を取得 - var pr = await _gitHubService.GetPullRequestAsync(owner, repo, prNumber); - var thresholdDescription = ApprovalThresholdHelper.GetDescription(threshold); - - // プロンプトを作成 - var autoApproveInstruction = autoApprove - ? "判断が承認(APPROVE)の場合は、approve_pull_request関数を呼び出して承認をバッファに追加してください。" - : "判断が承認(APPROVE)の場合は、DECISION: APPROVEと明確に記載してください。"; - - var prompt = $""" - コードレビューの結果に基づいて、このプルリクエストの承認判断を行ってください。 - - ## プルリクエスト - - タイトル: {pr.Title} - - 作成者: {pr.User.Login} - - ## コードレビュー結果 - {reviewResult} - - ## 承認基準 - {thresholdDescription} - - あなたのタスク: - 1. レビュー結果を承認基準と照らして分析 - 2. 判断を下してください(APPROVE、CHANGES_REQUESTED、COMMENT_ONLYのいずれか) - 3. {autoApproveInstruction} - 4. 判断がCHANGES_REQUESTEDの場合、request_changes関数を呼び出して変更依頼をバッファに追加してください - 5. 対処すべき懸念事項がある場合は、以下の関数を呼び出してください: - - post_pr_comment - 全般的なコメント - - post_line_comment - 特定行へのフィードバック - - post_range_comment - 複数行へのフィードバック(開始行と終了行を指定) - - post_review_comment - レビューレベルのコメント - - 重要: すべてのアクションはバッファに追加され、分析完了後に一括で実行されます。 - - 以下の形式で判断を記載してください: - - DECISION: [APPROVE/CHANGES_REQUESTED/COMMENT_ONLY] - REASONING: [判断理由を説明] - CONDITIONS: [マージ条件があれば記載、なければN/A] - APPROVAL_COMMENT: [承認時のコメント、なければN/A] - - 不確かな場合は、変更依頼またはコメントを追加してください。 - """; - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage(prompt); - - // FunctionCallingを有効にするために、Kernelから直接サービスを呼び出し - var kernel = agent.Kernel; - var chatService = kernel.GetRequiredService(); - - // OpenAI用の実行設定でFunctionCallingを有効化 - var executionSettings = new OpenAIPromptExecutionSettings - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() - }; - - var responses = new System.Text.StringBuilder(); - - // 関数呼び出しを含む可能性があるため、複数回の反復処理を行う - var maxIterations = 10; - var iteration = 0; - - while (iteration < maxIterations) - { - iteration++; - - // 非ストリーミングAPIで完全なレスポンスを取得 - var contents = await chatService.GetChatMessageContentsAsync( - chatHistory, - executionSettings, - kernel, - cancellationToken); - - var content = contents.FirstOrDefault(); - if (content == null) break; - - var currentResponse = content.Content ?? string.Empty; - responses.Append(currentResponse); - chatHistory.AddAssistantMessage(currentResponse); - - // 関数呼び出しが行われたかチェック - var hasFunctionCalls = content.Items?.Any(i => i is FunctionCallContent) == true; - - // 関数呼び出しがない場合はループを抜ける - if (!hasFunctionCalls) - { - break; - } - } - - var responseText = responses.ToString(); - - // レスポンスを解析 - var (shouldApprove, reasoning, comment) = ApprovalResponseParser.Parse(responseText); - - // バッファの内容を実行 - PRActionResult? actionResult = null; - var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber); - var state = buffer.GetState(); - - if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || - state.HasGeneralComment || state.ApprovalState != PRApprovalState.None) - { - actionResult = await executor.ExecuteAsync(buffer, cancellationToken); - } - - return (shouldApprove, reasoning, comment, actionResult); - } -} diff --git a/PRAgent/Agents/SK/SKReviewAgent.cs b/PRAgent/Agents/SK/SKReviewAgent.cs index 72e0995..d71df12 100644 --- a/PRAgent/Agents/SK/SKReviewAgent.cs +++ b/PRAgent/Agents/SK/SKReviewAgent.cs @@ -6,26 +6,31 @@ using PRAgent.Services; using PRAgent.Plugins.GitHub; using PRAgentDefinition = PRAgent.Agents.AgentDefinition; +using Octokit; 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) + IGitHubService gitHubService, + IDetailedCommentAgent detailedCommentAgent) { _agentFactory = agentFactory; _prDataService = prDataService; _gitHubService = gitHubService; + _detailedCommentAgent = detailedCommentAgent; } /// @@ -94,22 +99,8 @@ public async IAsyncEnumerable ReviewStreamingAsync( } /// - /// 指定された関数(プラグイン)を持つReviewエージェントを作成します - /// - public async Task CreateAgentWithFunctionsAsync( - string owner, - string repo, - int prNumber, - IEnumerable functions, - string? customSystemPrompt = null) - { - return await _agentFactory.CreateReviewAgentAsync( - owner, repo, prNumber, customSystemPrompt, functions); - } - - /// - /// バッファを使用して行コメント付きレビューを実行します - /// メインコメントは簡潔に保ち、詳細なフィードバックは行コメントとして投稿します + /// ReviewAgent(概要)とDetailedCommentAgent(詳細)を連携させたレビューを実行 + /// 一つのReviewとしてまとめて投稿 /// public async Task<(string ReviewText, PRActionResult? ActionResult)> ReviewWithLineCommentsAsync( string owner, @@ -118,42 +109,30 @@ public async Task CreateAgentWithFunctionsAsync( string? language = null, CancellationToken cancellationToken = default) { - // バッファを作成 - var buffer = new PRActionBuffer(); + // PRデータを取得 + var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); + var fileList = PullRequestDataService.FormatFileList(files); - // プラグインインスタンスを作成 - var commentPlugin = new PostCommentFunction(buffer); + // ===== バッファとKernelを準備 ===== + var buffer = new PRActionBuffer(); + var reviewSystemPrompt = GetReviewOverviewPrompt(language); - // カスタムシステムプロンプトを作成(簡潔なメインコメント+行コメント重視) - var systemPrompt = GetReviewWithLineCommentsPrompt(language); + // Kernelを作成してツールを登録 + var kernel = _agentFactory.CreateReviewKernel(owner, repo, prNumber, reviewSystemPrompt); - // Kernelを作成してプラグインを登録 - var kernel = _agentFactory.CreateApprovalKernel(owner, repo, prNumber, systemPrompt); - kernel.ImportPluginFromObject(commentPlugin); + // Approve/RequestChangesツールを登録 + var approvePlugin = new ApprovePRFunction(buffer); + kernel.Plugins.AddFromObject(approvePlugin, "pr_actions"); - // OpenAI用の実行設定でFunctionCallingを有効化 + // Function Calling設定 var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - // エージェントを作成(ArgumentsでFunctionCallingを有効化) - var agent = new ChatCompletionAgent - { - Name = AgentDefinition.ReviewAgent.Name, - Description = AgentDefinition.ReviewAgent.Description, - Instructions = systemPrompt, - Kernel = kernel, - Arguments = new KernelArguments(executionSettings) - }; - - // PRデータを取得 - var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); - var fileList = PullRequestDataService.FormatFileList(files); - - // プロンプトを作成 - var prompt = $""" - 以下のプルリクエストをコードレビューしてください。 + // ===== Step 1: ReviewAgentで概要を作成(Function Calling有効) ===== + var reviewPrompt = $""" + 以下のプルリクエストのコードレビューを行ってください。 ## プルリクエスト情報 - タイトル: {pr.Title} @@ -167,33 +146,58 @@ public async Task CreateAgentWithFunctionsAsync( {diff} ## レビュー指示 - 1. まず、post_review_comment関数を呼び出して、簡潔な全体レビュー(3-5行程度)を追加してください - 2. 各問題点に対して、post_line_comment関数を呼び出して行コメントを投稿してください - 3. 行コメントにはファイルパスと行番号を正確に指定してください - 4. 重大な問題には Critical、重要な問題には Major、軽微な問題には Minor のプレフィックスを付けてください - - 重要: メインのレビューコメントは簡潔に保ち、詳細なフィードバックは行コメントとして投稿してください。 + 1. 全体的な概要を3-5行程度で作成 + 2. 発見した問題点をリストアップ(各問題には重要度: Critical/Major/Minor を付与) + 3. レビュー完了後、以下のアクションをとってください: + - Criticalな問題がある場合: request_changes ツールを使用 + - 問題がない、またはMinorのみの場合: approve_pull_request ツールを使用 + + 出力形式: + ## 概要 + [全体的な概要] + + ## 発見した問題 + ### [Critical/Major/Minor] 問題タイトル + **ファイル:** `path/to/file.cs` + **行番号:** 45 + **説明:** 問題の詳細説明 + **修正提案:** + ```suggestion + // 修正内容 + ``` """; + var chatService = kernel.GetRequiredService(); var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage(prompt); + chatHistory.AddUserMessage(reviewPrompt); - var responses = new System.Text.StringBuilder(); - - // エージェントを実行(Function Callingは自動的に処理される) - await foreach (var response in agent.InvokeAsync(chatHistory, cancellationToken: cancellationToken)) + var reviewResponses = new System.Text.StringBuilder(); + await foreach (var content in chatService.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken)) { - responses.Append(response.Message.Content); + reviewResponses.Append(content.Content); } - var reviewText = responses.ToString(); + var reviewText = reviewResponses.ToString(); - // バッファの内容を実行 + // ===== Step 2: DetailedCommentAgentで詳細な行コメントを作成 ===== + var detailedComments = await _detailedCommentAgent.CreateCommentsAsync(reviewText, language ?? "en"); + + // ===== Step 3: バッファにコメントを追加 ===== + // 概要をレビューコメントとして追加 + buffer.AddReviewComment(reviewText); + + // 詳細な行コメントを追加 + foreach (var comment in detailedComments) + { + buffer.AddLineComment(comment.Path, comment.Line, comment.Body, comment.Suggestion); + } + + // ===== Step 4: アクションを実行 ===== PRActionResult? actionResult = null; var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber); var state = buffer.GetState(); - if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || state.HasGeneralComment) + if (state.LineCommentCount > 0 || state.ReviewCommentCount > 0 || state.ApprovalState != PRApprovalState.None) { actionResult = await executor.ExecuteAsync(buffer, cancellationToken); } @@ -202,9 +206,9 @@ 4. 重大な問題には Critical、重要な問題には Major、軽微な問 } /// - /// 言語に応じた行コメント重視のレビュープロンプトを取得します + /// 言語に応じた概要レビュープロンプトを取得します /// - private static string GetReviewWithLineCommentsPrompt(string? language) + private static string GetReviewOverviewPrompt(string? language) { var isJapanese = language?.ToLowerInvariant() == "ja"; @@ -213,26 +217,26 @@ private static string GetReviewWithLineCommentsPrompt(string? language) return """ あなたはシニアソフトウェアエンジニアとしてプルリクエストのコードレビューを行います。 - ## 重要なルール - 1. メインのレビューコメントは簡潔に(3-5行程度) - 2. 詳細なフィードバックは行コメントとして投稿 - 3. 各問題点に対して個別の行コメントを作成 - - ## 利用可能な関数 - - post_review_comment: 全体的なレビューコメント(簡潔に) - - post_line_comment: 特定の行にコメント(filePath, lineNumber, comment) - - post_range_comment: 複数行にコメント(filePath, startLine, endLine, comment) - - post_pr_comment: 全般的なコメント - - ## コメントの分類 - - [Critical]: 重大なバグ、セキュリティ問題 - - [Major]: 設計問題、パフォーマンス問題 - - [Minor]: スタイル、命名、軽微な改善 + ## 役割 + 1. コードの全体的な概要を作成(3-5行程度) + 2. 発見した問題点をリストアップ + 3. 各問題には重要度を付与(Critical/Major/Minor) ## 出力形式 - 1. まず post_review_comment で簡潔な全体レビューを投稿 - 2. 各問題点に対して post_line_comment で行コメントを投稿 - 3. ファイルパスと行番号を正確に指定すること + ## 概要 + [全体的な概要] + + ## 発見した問題 + ### [Critical/Major/Minor] 問題タイトル + **ファイル:** `path/to/file.cs` + **行番号:** 45 + **説明:** 問題の詳細説明 + **修正提案:** + ```suggestion + // 修正内容 + ``` + + 簡潔で建設的なフィードバックを心がけてください。 """; } else @@ -240,26 +244,26 @@ 3. ファイルパスと行番号を正確に指定すること return """ You are a senior software engineer performing code reviews on pull requests. - ## Important Rules - 1. Keep the main review comment concise (3-5 lines) - 2. Post detailed feedback as line comments - 3. Create individual line comments for each issue - - ## Available Functions - - post_review_comment: Overall review comment (keep concise) - - post_line_comment: Comment on specific line (filePath, lineNumber, comment) - - post_range_comment: Comment on multiple lines (filePath, startLine, endLine, comment) - - post_pr_comment: General comment - - ## Issue Classification - - [Critical]: Critical bugs, security issues - - [Major]: Design issues, performance problems - - [Minor]: Style, naming, minor improvements + ## 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 - 1. First, post a concise overall review using post_review_comment - 2. For each issue, post a line comment using post_line_comment - 3. Ensure filePath and lineNumber are accurate + ## 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. """; } } diff --git a/PRAgent/Agents/SK/SKSummaryAgent.cs b/PRAgent/Agents/SK/SKSummaryAgent.cs deleted file mode 100644 index e8b4b55..0000000 --- a/PRAgent/Agents/SK/SKSummaryAgent.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; -using PRAgent.Services; -using PRAgentDefinition = PRAgent.Agents.AgentDefinition; - -namespace PRAgent.Agents.SK; - -/// -/// Semantic Kernel ChatCompletionAgentベースのサマリーエージェント -/// -public class SKSummaryAgent -{ - private readonly PRAgentFactory _agentFactory; - private readonly PullRequestDataService _prDataService; - - public SKSummaryAgent( - PRAgentFactory agentFactory, - PullRequestDataService prDataService) - { - _agentFactory = agentFactory; - _prDataService = prDataService; - } - - /// - /// プルリクエストの要約を作成します - /// - public async Task SummarizeAsync( - string owner, - string repo, - int prNumber, - string? customSystemPrompt = null, - CancellationToken cancellationToken = default) - { - // Summaryエージェントを作成 - var agent = await _agentFactory.CreateSummaryAgentAsync(owner, repo, prNumber, customSystemPrompt); - - // PRデータを取得 - var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); - var fileList = PullRequestDataService.FormatFileList(files); - - // プロンプトを作成 - var systemPrompt = customSystemPrompt ?? PRAgentDefinition.SummaryAgent.SystemPrompt; - var prompt = PullRequestDataService.CreateSummaryPrompt(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 SummarizeStreamingAsync( - string owner, - string repo, - int prNumber, - string? customSystemPrompt = null, - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Summaryエージェントを作成 - var agent = await _agentFactory.CreateSummaryAgentAsync(owner, repo, prNumber, customSystemPrompt); - - // PRデータを取得 - var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(owner, repo, prNumber); - var fileList = PullRequestDataService.FormatFileList(files); - - // プロンプトを作成 - var systemPrompt = customSystemPrompt ?? PRAgentDefinition.SummaryAgent.SystemPrompt; - var prompt = PullRequestDataService.CreateSummaryPrompt(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; - } - } - - /// - /// 指定された関数(プラグイン)を持つSummaryエージェントを作成します - /// - public async Task CreateAgentWithFunctionsAsync( - string owner, - string repo, - int prNumber, - IEnumerable functions, - string? customSystemPrompt = null) - { - return await _agentFactory.CreateSummaryAgentAsync( - owner, repo, prNumber, customSystemPrompt, functions); - } -} diff --git a/PRAgent/Configuration/ServiceCollectionExtensions.cs b/PRAgent/Configuration/ServiceCollectionExtensions.cs index f0eebc1..cc41b56 100644 --- a/PRAgent/Configuration/ServiceCollectionExtensions.cs +++ b/PRAgent/Configuration/ServiceCollectionExtensions.cs @@ -73,8 +73,6 @@ public static IServiceCollection AddPRAgentServices( // SK Agents (Semantic Kernel Agent Framework) services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); // Agent Orchestrator - SKAgentOrchestratorServiceを使用 services.AddSingleton(); diff --git a/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs b/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs index de80acf..6940114 100644 --- a/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs +++ b/PRAgent/Plugins/Agent/AgentInvocationFunctions.cs @@ -9,7 +9,7 @@ namespace PRAgent.Plugins.Agent; /// /// Agent-as-Functionパターンを実装するプラグイン -/// 他のエージェントを関数として呼び出すことを可能にします +/// ReviewAgentを関数として呼び出すことを可能にします /// public class AgentInvocationFunctions { @@ -76,49 +76,6 @@ public async Task InvokeReviewAgentAsync( } } - /// - /// Summaryエージェントを呼び出して要約を作成します - /// - /// カスタムプロンプト(オプション) - /// キャンセレーショントークン - /// 要約結果 - [KernelFunction("invoke_summary_agent")] - public async Task InvokeSummaryAgentAsync( - string? customPrompt = null, - CancellationToken cancellationToken = default) - { - try - { - // Summaryエージェントを作成 - var agent = await _agentFactory.CreateSummaryAgentAsync(_owner, _repo, _prNumber); - - // PRデータを取得 - var (pr, files, diff) = await _prDataService.GetPullRequestDataAsync(_owner, _repo, _prNumber); - var fileList = PullRequestDataService.FormatFileList(files); - - // プロンプトを作成 - var prompt = string.IsNullOrEmpty(customPrompt) - ? PullRequestDataService.CreateSummaryPrompt(pr, fileList, diff, PRAgentDefinition.SummaryAgent.SystemPrompt) - : customPrompt; - - // チャット履歴を作成してエージェントを実行 - 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(); - } - catch (Exception ex) - { - return $"Error invoking summary agent: {ex.Message}"; - } - } - /// /// Reviewエージェントを関数として呼び出すためのKernelFunctionを作成します /// @@ -144,138 +101,4 @@ public static KernelFunction InvokeReviewAgentFunction( } }); } - - /// - /// Summaryエージェントを関数として呼び出すためのKernelFunctionを作成します - /// - public static KernelFunction InvokeSummaryAgentFunction( - PRAgentFactory agentFactory, - PullRequestDataService prDataService, - string owner, - string repo, - int prNumber) - { - var invocationPlugin = new AgentInvocationFunctions(agentFactory, prDataService, owner, repo, prNumber); - return KernelFunctionFactory.CreateFromMethod( - (string? customPrompt, CancellationToken ct) => invocationPlugin.InvokeSummaryAgentAsync(customPrompt, ct), - functionName: "invoke_summary_agent", - description: "Invokes the summary agent to create a summary of a pull request", - parameters: new[] - { - new KernelParameterMetadata("customPrompt") - { - Description = "Optional custom prompt for the summary agent", - IsRequired = false, - DefaultValue = null - } - }); - } - - /// - /// Approvalエージェントを呼び出して承認決定を行います - /// - /// レビュー結果 - /// 承認閾値 - /// キャンセレーショントークン - /// 承認決定結果 - [KernelFunction("invoke_approval_agent")] - public async Task InvokeApprovalAgentAsync( - string reviewResult, - string threshold = "major", - CancellationToken cancellationToken = default) - { - try - { - // Approvalエージェントを作成 - var agent = await _agentFactory.CreateApprovalAgentAsync(_owner, _repo, _prNumber); - - // PRデータを取得 - var pr = await _prDataService.GetPullRequestDataAsync(_owner, _repo, _prNumber); - - // プロンプトを作成 - var thresholdDescription = GetThresholdDescription(threshold); - var prompt = $""" - Based on the code review below, make an approval decision for this pull request. - - ## Pull Request - - Title: {pr.Item1.Title} - - Author: {pr.Item1.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 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(); - } - catch (Exception ex) - { - return $"Error invoking approval agent: {ex.Message}"; - } - } - - private static string GetThresholdDescription(string threshold) - { - return threshold.ToLower() switch - { - "critical" => "critical: PR must have NO critical issues", - "major" => "major: PR must have NO major or critical issues", - "minor" => "minor: PR must have NO minor, major, or critical issues", - "none" => "none: Always approve", - _ => "major: PR must have NO major or critical issues (default)" - }; - } - - /// - /// Approvalエージェントを関数として呼び出すためのKernelFunctionを作成します - /// - public static KernelFunction InvokeApprovalAgentFunction( - PRAgentFactory agentFactory, - PullRequestDataService prDataService, - string owner, - string repo, - int prNumber) - { - var invocationPlugin = new AgentInvocationFunctions(agentFactory, prDataService, owner, repo, prNumber); - return KernelFunctionFactory.CreateFromMethod( - (string reviewResult, string threshold, CancellationToken ct) => - invocationPlugin.InvokeApprovalAgentAsync(reviewResult, threshold, ct), - functionName: "invoke_approval_agent", - description: "Invokes the approval agent to make an approval decision based on review results", - parameters: new[] - { - new KernelParameterMetadata("reviewResult") - { - Description = "The code review results to evaluate", - IsRequired = true - }, - new KernelParameterMetadata("threshold") - { - Description = "The approval threshold (critical, major, minor, none)", - IsRequired = false, - DefaultValue = "major" - } - }); - } } diff --git a/PRAgent/Program.cs b/PRAgent/Program.cs index 4accf36..d3c682d 100644 --- a/PRAgent/Program.cs +++ b/PRAgent/Program.cs @@ -93,8 +93,6 @@ static async Task Main(string[] args) // SK Agents (Semantic Kernel Agent Framework) services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); // Agent Orchestrator - SKAgentOrchestratorServiceを使用 services.AddSingleton(); diff --git a/PRAgent/Services/IAgentOrchestratorService.cs b/PRAgent/Services/IAgentOrchestratorService.cs index 3851bcb..49441b8 100644 --- a/PRAgent/Services/IAgentOrchestratorService.cs +++ b/PRAgent/Services/IAgentOrchestratorService.cs @@ -4,6 +4,7 @@ namespace PRAgent.Services; /// /// エージェントオーケストレーションサービスのインターフェース +/// ReviewAgentを中心に簡素化された構成 /// public interface IAgentOrchestratorService { @@ -13,9 +14,9 @@ public interface IAgentOrchestratorService Task ReviewAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default); /// - /// プルリクエストの要約を作成します + /// プルリクエストのコードレビューを実行します(language指定) /// - Task SummarizeAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default); + Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default); /// /// レビューと承認を一連のワークフローとして実行します @@ -27,16 +28,6 @@ Task ReviewAndApproveAsync( ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default); - /// - /// プルリクエストのコードレビューを実行します(language指定) - /// - Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default); - - /// - /// プルリクエストの要約を作成します(language指定) - /// - Task SummarizeAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default); - /// /// レビューと承認を一連のワークフローとして実行します(language指定) /// diff --git a/PRAgent/Services/IDetailedCommentAgent.cs b/PRAgent/Services/IDetailedCommentAgent.cs index 03d2f4f..707eefa 100644 --- a/PRAgent/Services/IDetailedCommentAgent.cs +++ b/PRAgent/Services/IDetailedCommentAgent.cs @@ -1,9 +1,7 @@ -using Octokit; - namespace PRAgent.Services; /// -/// 詳細な行コメントを作成するエージェントのインターフェース +/// 詳細な行コメントを作するエージェントのインターフェース /// public interface IDetailedCommentAgent { @@ -12,11 +10,21 @@ public interface IDetailedCommentAgent /// /// レビュー結果の文字列 /// 出力言語 - /// GitHubレビュー用のコメントリスト - Task> CreateCommentsAsync(string review, string language); + /// 行コメントのリスト + Task> CreateCommentsAsync(string review, string language); /// /// 言語を動的に設定 /// void SetLanguage(string language); -} \ No newline at end of file +} + +/// +/// 行コメントデータ/// +public class LineCommentData +{ + public required string Path { get; init; } + public required int Line { get; init; } + public required string Body { get; init; } + public string? Suggestion { get; init; } +} diff --git a/PRAgent/Services/PRAnalysisService.cs b/PRAgent/Services/PRAnalysisService.cs index cc7d32f..a02c15e 100644 --- a/PRAgent/Services/PRAnalysisService.cs +++ b/PRAgent/Services/PRAnalysisService.cs @@ -63,14 +63,10 @@ public async Task SummarizePullRequestAsync(string owner, string repo, i return "PRAgent is disabled for this repository."; } - if (config.Summary == null || !config.Summary.Enabled) - { - return "Summary is disabled for this repository."; - } - + // SummaryAgentは廃止されたため、ReviewAgentを使用 var summary = !string.IsNullOrEmpty(language) - ? await _agentOrchestrator.SummarizeAsync(owner, repo, prNumber, language) - : await _agentOrchestrator.SummarizeAsync(owner, repo, prNumber); + ? await _agentOrchestrator.ReviewAsync(owner, repo, prNumber, language) + : await _agentOrchestrator.ReviewAsync(owner, repo, prNumber); if (postComment) { diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index e850a27..28068c6 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -1,45 +1,29 @@ using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Chat; -using Microsoft.SemanticKernel.ChatCompletion; -using PRAgent.Agents; using PRAgent.Agents.SK; using PRAgent.Models; -using PRAgent.Plugins.Agent; -using PRAgent.Plugins.GitHub; -using PRAgentDefinition = PRAgent.Agents.AgentDefinition; namespace PRAgent.Services.SK; /// -/// Semantic Kernel AgentGroupChatを使用したエージェントオーケストレーションサービス +/// Semantic Kernelを使用したエージェントオーケストレーションサービス +/// ReviewAgentを中心に簡素化された構成 /// public class SKAgentOrchestratorService : IAgentOrchestratorService { - private readonly PRAgentFactory _agentFactory; private readonly SKReviewAgent _reviewAgent; - private readonly SKSummaryAgent _summaryAgent; - private readonly SKApprovalAgent _approvalAgent; private readonly IGitHubService _gitHubService; private readonly PullRequestDataService _prDataService; private readonly PRAgentConfig _config; private readonly ILogger _logger; public SKAgentOrchestratorService( - PRAgentFactory agentFactory, SKReviewAgent reviewAgent, - SKSummaryAgent summaryAgent, - SKApprovalAgent approvalAgent, IGitHubService gitHubService, PullRequestDataService prDataService, PRAgentConfig config, ILogger logger) { - _agentFactory = agentFactory; _reviewAgent = reviewAgent; - _summaryAgent = summaryAgent; - _approvalAgent = approvalAgent; _gitHubService = gitHubService; _prDataService = prDataService; _config = config; @@ -56,16 +40,17 @@ public async Task ReviewAsync(string owner, string repo, int prNumber, C if (useFunctionCalling) { - // 行コメント付きレビューを実行 + // 行コメント付きレビューを実行(承認機能含む) var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( owner, repo, prNumber, language: null, cancellationToken); if (actionResult != null) { _logger.LogInformation( - "Review with line comments completed. Line comments: {LineComments}, Review comments: {ReviewComments}", + "Review completed. Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", actionResult.LineCommentsPosted, - actionResult.ReviewCommentsPosted); + actionResult.ReviewCommentsPosted, + actionResult.Approved); } return reviewText; @@ -75,86 +60,69 @@ public async Task ReviewAsync(string owner, string repo, int prNumber, C } /// - /// プルリクエストの要約を作成します + /// プルリクエストのコードレビューを実行します(language指定) /// - public async Task SummarizeAsync(string owner, string repo, int prNumber, CancellationToken cancellationToken = default) + public async Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) { - return await _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + + if (useFunctionCalling) + { + var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( + owner, repo, prNumber, language, cancellationToken); + + if (actionResult != null) + { + _logger.LogInformation( + "Review completed. Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", + actionResult.LineCommentsPosted, + actionResult.ReviewCommentsPosted, + actionResult.Approved); + } + + return reviewText; + } + + return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); } /// /// レビューと承認を一連のワークフローとして実行します + /// ReviewAgentに統合されたため、ReviewWithLineCommentsAsyncを使用 /// public async Task ReviewAndApproveAsync( string owner, string repo, int prNumber, - ApprovalThreshold threshold, + ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default) { - // ワークフロー: ReviewAgent → ApprovalAgent - var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - - // FunctionCalling設定に応じてメソッドを選択 var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; - var autoApprove = _config.AgentFramework?.EnableAutoApproval ?? false; - - bool shouldApprove; - string reasoning; - string? comment; - string? approvalUrl = null; if (useFunctionCalling) { - // FunctionCalling使用 - PRActionResultが返される - var (_, reasoningFc, commentFc, actionResult) = await _approvalAgent.DecideWithFunctionCallingAsync( - owner, repo, prNumber, review, threshold, autoApprove, language: null, cancellationToken); - shouldApprove = actionResult?.Approved ?? false; - reasoning = reasoningFc; - comment = commentFc; - approvalUrl = actionResult?.ApprovalUrl; - } - else - { - // FunctionCalling不使用 - (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( - owner, repo, prNumber, review, threshold, cancellationToken); + var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( + owner, repo, prNumber, language: null, cancellationToken); - if (shouldApprove) + return new ApprovalResult { - var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); - approvalUrl = result; - } + Approved = actionResult?.Approved ?? false, + Review = reviewText, + Reasoning = actionResult?.Message ?? string.Empty, + ApprovalUrl = actionResult?.ApprovalUrl + }; } + // FunctionCalling無効の場合は通常のレビューのみ + var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); return new ApprovalResult { - Approved = shouldApprove, + Approved = false, Review = review, - Reasoning = reasoning, - Comment = comment, - ApprovalUrl = approvalUrl + Reasoning = "Function calling is disabled. Manual approval required." }; } - /// - /// プルリクエストのコードレビューを実行します(language指定) - /// - public async Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) - { - // languageパラメータは現在のSK実装では使用しない - return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - } - - /// - /// プルリクエストの要約を作成します(language指定) - /// - public async Task SummarizeAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) - { - // languageパラメータは現在のSK実装では使用しない - return await _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - } - /// /// レビューと承認を一連のワークフローとして実行します(language指定) /// @@ -166,43 +134,42 @@ public async Task ReviewAndApproveAsync( ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default) { - // languageパラメータは現在のSK実装では使用しない - return await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken); - } + var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; - /// - /// AgentGroupChatを使用したマルチエージェント協調によるレビューと承認 - /// - public async Task ReviewAndApproveWithAgentChatAsync( - string owner, - string repo, - int prNumber, - ApprovalThreshold threshold, - CancellationToken cancellationToken = default) - { - // 設定されたオーケストレーションモードに応じて処理を分岐 - var orchestrationMode = _config.AgentFramework?.OrchestrationMode ?? "sequential"; + if (useFunctionCalling) + { + var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( + owner, repo, prNumber, language, cancellationToken); - return orchestrationMode.ToLower() switch + return new ApprovalResult + { + Approved = actionResult?.Approved ?? false, + Review = reviewText, + Reasoning = actionResult?.Message ?? string.Empty, + ApprovalUrl = actionResult?.ApprovalUrl + }; + } + + var review = await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); + return new ApprovalResult { - "agent_chat" => await ExecuteWithAgentGroupChatAsync(owner, repo, prNumber, threshold, cancellationToken), - "sequential" => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken), - _ => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken) + Approved = false, + Review = review, + Reasoning = "Function calling is disabled. Manual approval required." }; } /// - /// AgentGroupChatを使用した実行 - /// 注: Semantic Kernel 1.68.0のAgentGroupChat APIは複雑なため、簡易実装としています + /// AgentGroupChatを使用したマルチエージェント協調によるレビューと承認 + /// 現在はReviewAgentに統合されたため、ReviewAndApproveAsyncと同じ /// - private async Task ExecuteWithAgentGroupChatAsync( + public async Task ReviewAndApproveWithAgentChatAsync( string owner, string repo, int prNumber, - ApprovalThreshold threshold, - CancellationToken cancellationToken) + ApprovalThreshold threshold = ApprovalThreshold.Minor, + CancellationToken cancellationToken = default) { - // 現在はSequentialモードとして実行(将来的に完全なAgentGroupChatを実装) return await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken); } @@ -213,129 +180,11 @@ public async Task ReviewAndApproveWithCustomWorkflowAsync( string owner, string repo, int prNumber, - string workflow, + string workflowType, ApprovalThreshold threshold = ApprovalThreshold.Minor, CancellationToken cancellationToken = default) { - return workflow.ToLower() switch - { - "collaborative" => await CollaborativeReviewWorkflowAsync(owner, repo, prNumber, threshold, cancellationToken), - "parallel" => await ParallelReviewWorkflowAsync(owner, repo, prNumber, threshold, cancellationToken), - "sequential" => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken), - _ => await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken) - }; - } - - /// - /// 協調型レビューワークフロー - SummaryとReviewの両方を実行 - /// - private async Task CollaborativeReviewWorkflowAsync( - string owner, - string repo, - int prNumber, - ApprovalThreshold threshold, - CancellationToken cancellationToken) - { - // SummaryとReviewを並行して実行 - var summaryTask = _summaryAgent.SummarizeAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - var reviewTask = _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); - - await Task.WhenAll(summaryTask, reviewTask); - - var summary = await summaryTask; - var review = await reviewTask; - - // 要約を含むレビュー結果を作成 - var enhancedReview = $""" - {review} - - ## Summary - {summary} - """; - - var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( - owner, repo, prNumber, enhancedReview, threshold, cancellationToken); - - string? approvalUrl = null; - if (shouldApprove) - { - var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); - approvalUrl = result; - } - - return new ApprovalResult - { - Approved = shouldApprove, - Review = enhancedReview, - Reasoning = reasoning, - Comment = comment, - ApprovalUrl = approvalUrl - }; - } - - /// - /// 並列レビューワークフロー - 複数の視点からレビュー - /// - private async Task ParallelReviewWorkflowAsync( - string owner, - string repo, - int prNumber, - ApprovalThreshold threshold, - CancellationToken cancellationToken) - { - // 様々な視点でレビューを実行 - var securityReviewTask = _reviewAgent.ReviewAsync( - owner, repo, prNumber, - "Focus specifically on security vulnerabilities, authentication issues, and data protection concerns.", - cancellationToken); - - var performanceReviewTask = _reviewAgent.ReviewAsync( - owner, repo, prNumber, - "Focus specifically on performance implications, scalability concerns, and resource usage.", - cancellationToken); - - var codeQualityReviewTask = _reviewAgent.ReviewAsync( - owner, repo, prNumber, - "Focus specifically on code quality, maintainability, and adherence to best practices.", - cancellationToken); - - await Task.WhenAll(securityReviewTask, performanceReviewTask, codeQualityReviewTask); - - var securityReview = await securityReviewTask; - var performanceReview = await performanceReviewTask; - var codeQualityReview = await codeQualityReviewTask; - - // 統合レビューを作成 - var combinedReview = $""" - ## Combined Code Review - - ### Security Review - {securityReview} - - ### Performance Review - {performanceReview} - - ### Code Quality Review - {codeQualityReview} - """; - - var (shouldApprove, reasoning, comment) = await _approvalAgent.DecideAsync( - owner, repo, prNumber, combinedReview, threshold, cancellationToken); - - string? approvalUrl = null; - if (shouldApprove) - { - var result = await _approvalAgent.ApproveAsync(owner, repo, prNumber, comment); - approvalUrl = result; - } - - return new ApprovalResult - { - Approved = shouldApprove, - Review = combinedReview, - Reasoning = reasoning, - Comment = comment, - ApprovalUrl = approvalUrl - }; + // すべてのワークフローはReviewAgentに統合 + return await ReviewAndApproveAsync(owner, repo, prNumber, threshold, cancellationToken); } } From 78040fd280dcae4914578cd5ca12e027b1703377 Mon Sep 17 00:00:00 2001 From: actbit <57023457+actbit@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:53:14 +0900 Subject: [PATCH 27/27] [add] not use subagent --- PRAgent/Agents/DetailedCommentAgent.cs | 165 ++++++++-- PRAgent/Agents/SK/SKReviewAgent.cs | 302 +++++++++++++++--- PRAgent/Models/PRAgentYmlConfig.cs | 7 + PRAgent/Services/IDetailedCommentAgent.cs | 44 ++- .../Services/SK/SKAgentOrchestratorService.cs | 101 ++++-- 5 files changed, 527 insertions(+), 92 deletions(-) diff --git a/PRAgent/Agents/DetailedCommentAgent.cs b/PRAgent/Agents/DetailedCommentAgent.cs index f0b5e91..416080d 100644 --- a/PRAgent/Agents/DetailedCommentAgent.cs +++ b/PRAgent/Agents/DetailedCommentAgent.cs @@ -1,14 +1,13 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; -using PRAgent.Models; using PRAgent.Services; namespace PRAgent.Agents; /// /// 詳細な行コメントを作成するサブエージェント -/// ReviewAgentが作成した概要から、具体的な行コメントを生成 +/// ReviewAgentからFunction Callingで呼び出され、個々の問題について詳細なコメントを生成 /// public class DetailedCommentAgent : IDetailedCommentAgent { @@ -33,7 +32,103 @@ public void SetLanguage(string language) } /// - /// レビュー概要から詳細な行コメントを作成 + /// 個々の問題について詳細なコメントを生成(Function Calling用) + /// + /// ファイルパス + /// 行番号 + /// 周辺コード + /// 問題の概要 + /// 詳細なコメント(JSON形式) + [KernelFunction("get_detailed_comment")] + public async Task GetDetailedCommentAsync( + string filePath, + int lineNumber, + string codeSnippet, + string issueSummary) + { + var systemPrompt = GetDetailedCommentPrompt(_language); + var kernel = _kernelService.CreateAgentKernel(systemPrompt); + + var prompt = $$""" + 以下のコードの問題点について、GitHubのプルリクエストレビュー用の詳細なコメントを作成してください。 + + ## ファイル情報 + - ファイルパス: {{filePath}} + - 行番号: {{lineNumber}} + + ## 対象コード + ``` + {{codeSnippet}} + ``` + + ## 問題の概要 + {{issueSummary}} + + ## 出力形式(JSON) + 以下の形式で出力してください: + ```json + { + "path": "{{filePath}}", + "line": {{lineNumber}}, + "body": "詳細なコメント本文", + "suggestion": "修正提案のコード(あれば)" + } + ``` + + 重要: 必ず有効な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; + } + + /// + /// 複数の問題について一括で詳細コメントを生成(バッチ処理用) + /// + public async Task> CreateDetailedCommentsAsync( + List issues, + string language) + { + SetLanguage(language); + var results = new List(); + + foreach (var issue in issues) + { + try + { + var jsonResult = await GetDetailedCommentAsync( + issue.FilePath, + issue.LineNumber, + issue.CodeSnippet, + issue.IssueSummary); + + var comment = ParseSingleCommentFromJson(jsonResult); + if (comment != null) + { + results.Add(comment); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create detailed comment for {File}:{Line}", + issue.FilePath, issue.LineNumber); + } + } + + return results; + } + + /// + /// レビュー概要から詳細な行コメントを作成(従来のメソッド、互換性のため残す) /// public async Task> CreateCommentsAsync(string reviewOverview, string language) { @@ -77,12 +172,42 @@ 3. 修正提案がある場合は suggestion ブロックを含める _logger.LogInformation("=== DetailedCommentAgent Response ===\n{Response}", responseText); - // JSONをパース return ParseCommentsFromJson(responseText); } /// - /// JSONから行コメントデータをパース + /// 単一のJSONオブジェクトから行コメントデータをパース + /// + private LineCommentData? ParseSingleCommentFromJson(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(); + } + + // 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; + } + } + + /// + /// JSON配列から行コメントデータをパース /// private List ParseCommentsFromJson(string json) { @@ -128,16 +253,17 @@ private static string GetDetailedCommentPrompt(string language) あなたはGitHubのプルリクエストレビュー用の詳細コメントを作成する専門家です。 ## 役割 - レビュー概要から、具体的な行コメントを抽出・生成 + 指定されたコードの問題点について、詳細なコメントを生成 ## 出力ルール - 1. 有効なJSON配列のみを出力 - 2. 各コメントには以下を含める: - - path: ファイルパス - - line: 行番号 - - body: コメント本文 + 1. 有効なJSONオブジェクトのみを出力 + 2. コメントには以下を含める: + - path: ファイルパス(入力と同じ値) + - line: 行番号(入力と同じ値) + - body: 詳細なコメント本文 - suggestion: 修正提案(オプション) - 3. コメントは簡潔かつ建設的に + 3. コメントは建設的で、修正方法を具体的に提示 + 4. コードスニペットを参照して、具体的な改善案を提示 """; } else @@ -146,16 +272,17 @@ 3. コメントは簡潔かつ建設的に You are an expert at creating detailed line comments for GitHub pull request reviews. ## Your Role - Extract and generate specific line comments from review overview. + Generate detailed comments for specified code issues. ## Output Rules - 1. Output only valid JSON array - 2. Each comment should include: - - path: file path - - line: line number - - body: comment body + 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 concise and constructive + 3. Keep comments constructive with specific improvement suggestions + 4. Reference the code snippet and provide concrete fixes """; } } diff --git a/PRAgent/Agents/SK/SKReviewAgent.cs b/PRAgent/Agents/SK/SKReviewAgent.cs index d71df12..06789cd 100644 --- a/PRAgent/Agents/SK/SKReviewAgent.cs +++ b/PRAgent/Agents/SK/SKReviewAgent.cs @@ -6,7 +6,6 @@ using PRAgent.Services; using PRAgent.Plugins.GitHub; using PRAgentDefinition = PRAgent.Agents.AgentDefinition; -using Octokit; namespace PRAgent.Agents.SK; @@ -100,7 +99,7 @@ public async IAsyncEnumerable ReviewStreamingAsync( /// /// ReviewAgent(概要)とDetailedCommentAgent(詳細)を連携させたレビューを実行 - /// 一つのReviewとしてまとめて投稿 + /// Function CallingでDetailedCommentAgentを呼び出し /// public async Task<(string ReviewText, PRActionResult? ActionResult)> ReviewWithLineCommentsAsync( string owner, @@ -115,14 +114,19 @@ public async IAsyncEnumerable ReviewStreamingAsync( // ===== バッファとKernelを準備 ===== var buffer = new PRActionBuffer(); - var reviewSystemPrompt = GetReviewOverviewPrompt(language); + _detailedCommentAgent.SetLanguage(language ?? "en"); + + var reviewSystemPrompt = GetReviewWithSubAgentPrompt(language); // Kernelを作成してツールを登録 var kernel = _agentFactory.CreateReviewKernel(owner, repo, prNumber, reviewSystemPrompt); - // Approve/RequestChangesツールを登録 + // ツールを登録 + 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 @@ -130,7 +134,7 @@ public async IAsyncEnumerable ReviewStreamingAsync( FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - // ===== Step 1: ReviewAgentで概要を作成(Function Calling有効) ===== + // ===== ReviewAgentでレビュー実行(Function CallingでSubAgent呼び出し) ===== var reviewPrompt = $""" 以下のプルリクエストのコードレビューを行ってください。 @@ -146,25 +150,32 @@ public async IAsyncEnumerable ReviewStreamingAsync( {diff} ## レビュー指示 - 1. 全体的な概要を3-5行程度で作成 - 2. 発見した問題点をリストアップ(各問題には重要度: Critical/Major/Minor を付与) - 3. レビュー完了後、以下のアクションをとってください: - - Criticalな問題がある場合: request_changes ツールを使用 - - 問題がない、またはMinorのみの場合: approve_pull_request ツールを使用 - - 出力形式: - ## 概要 - [全体的な概要] - - ## 発見した問題 - ### [Critical/Major/Minor] 問題タイトル - **ファイル:** `path/to/file.cs` - **行番号:** 45 - **説明:** 問題の詳細説明 - **修正提案:** - ```suggestion - // 修正内容 - ``` + 利用可能なツールを使ってレビューを行ってください: + + 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(); @@ -179,20 +190,7 @@ 1. 全体的な概要を3-5行程度で作成 var reviewText = reviewResponses.ToString(); - // ===== Step 2: DetailedCommentAgentで詳細な行コメントを作成 ===== - var detailedComments = await _detailedCommentAgent.CreateCommentsAsync(reviewText, language ?? "en"); - - // ===== Step 3: バッファにコメントを追加 ===== - // 概要をレビューコメントとして追加 - buffer.AddReviewComment(reviewText); - - // 詳細な行コメントを追加 - foreach (var comment in detailedComments) - { - buffer.AddLineComment(comment.Path, comment.Line, comment.Body, comment.Suggestion); - } - - // ===== Step 4: アクションを実行 ===== + // ===== アクションを実行 ===== PRActionResult? actionResult = null; var executor = new PRActionExecutor(_gitHubService, owner, repo, prNumber); var state = buffer.GetState(); @@ -267,4 +265,232 @@ 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/Models/PRAgentYmlConfig.cs b/PRAgent/Models/PRAgentYmlConfig.cs index 7b6b619..f241ce2 100644 --- a/PRAgent/Models/PRAgentYmlConfig.cs +++ b/PRAgent/Models/PRAgentYmlConfig.cs @@ -75,6 +75,13 @@ public class AgentFrameworkConfig /// public bool EnableAutoApproval { get; set; } = false; + /// + /// SubAgent(DetailedCommentAgent)を使用するかどうか + /// true: ReviewAgentがFunction CallingでDetailedCommentAgentを呼び出し + /// false: ReviewAgentだけで完結(Function Callingで直接コメント投稿) + /// + public bool UseSubAgent { get; set; } = false; + /// /// 最大ターン数(AgentGroupChat用) /// diff --git a/PRAgent/Services/IDetailedCommentAgent.cs b/PRAgent/Services/IDetailedCommentAgent.cs index 707eefa..12fd9c5 100644 --- a/PRAgent/Services/IDetailedCommentAgent.cs +++ b/PRAgent/Services/IDetailedCommentAgent.cs @@ -1,26 +1,45 @@ namespace PRAgent.Services; /// -/// 詳細な行コメントを作するエージェントのインターフェース +/// 詳細な行コメントを作成するエージェントのインターフェース /// public interface IDetailedCommentAgent { /// - /// レビュー結果から詳細な行コメントを作成 + /// 言語を設定 /// - /// レビュー結果の文字列 + void SetLanguage(string language); + + /// + /// 個々の問題について詳細なコメントを生成(Function Calling用) + /// + /// ファイルパス + /// 行番号 + /// 周辺コード + /// 問題の概要 + /// 詳細なコメント(JSON形式) + Task GetDetailedCommentAsync(string filePath, int lineNumber, string codeSnippet, string issueSummary); + + /// + /// 複数の問題について一括で詳細コメントを生成 + /// + /// 問題のリスト /// 出力言語 /// 行コメントのリスト - Task> CreateCommentsAsync(string review, string language); + Task> CreateDetailedCommentsAsync(List issues, string language); /// - /// 言語を動的に設定 + /// レビュー概要から詳細な行コメントを作成(従来のメソッド) /// - void SetLanguage(string language); + /// レビュー結果の文字列 + /// 出力言語 + /// 行コメントのリスト + Task> CreateCommentsAsync(string reviewOverview, string language); } /// -/// 行コメントデータ/// +/// 行コメントデータ +/// public class LineCommentData { public required string Path { get; init; } @@ -28,3 +47,14 @@ public class LineCommentData public required string Body { get; init; } public string? Suggestion { get; init; } } + +/// +/// 問題のコンテキスト情報 +/// +public class IssueContext +{ + public required string FilePath { get; init; } + public required int LineNumber { get; init; } + public required string CodeSnippet { get; init; } + public required string IssueSummary { get; init; } +} diff --git a/PRAgent/Services/SK/SKAgentOrchestratorService.cs b/PRAgent/Services/SK/SKAgentOrchestratorService.cs index 28068c6..d7c2c34 100644 --- a/PRAgent/Services/SK/SKAgentOrchestratorService.cs +++ b/PRAgent/Services/SK/SKAgentOrchestratorService.cs @@ -37,23 +37,44 @@ public async Task ReviewAsync(string owner, string repo, int prNumber, C { // FunctionCalling設定に応じてメソッドを選択 var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + var useSubAgent = _config.AgentFramework?.UseSubAgent ?? true; if (useFunctionCalling) { - // 行コメント付きレビューを実行(承認機能含む) - var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( - owner, repo, prNumber, language: null, cancellationToken); - - if (actionResult != null) + if (useSubAgent) { - _logger.LogInformation( - "Review completed. Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", - actionResult.LineCommentsPosted, - actionResult.ReviewCommentsPosted, - actionResult.Approved); + // SubAgentあり: ReviewAgentで概要 → DetailedCommentAgentで詳細 + var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( + owner, repo, prNumber, language: null, cancellationToken); + + if (actionResult != null) + { + _logger.LogInformation( + "Review completed (with SubAgent). Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", + actionResult.LineCommentsPosted, + actionResult.ReviewCommentsPosted, + actionResult.Approved); + } + + return reviewText; + } + else + { + // SubAgentなし: ReviewAgentだけで完結 + var (reviewText, actionResult) = await _reviewAgent.ReviewDirectAsync( + owner, repo, prNumber, language: null, cancellationToken); + + if (actionResult != null) + { + _logger.LogInformation( + "Review completed (direct mode). Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", + actionResult.LineCommentsPosted, + actionResult.ReviewCommentsPosted, + actionResult.Approved); + } + + return reviewText; } - - return reviewText; } return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); @@ -65,22 +86,42 @@ public async Task ReviewAsync(string owner, string repo, int prNumber, C public async Task ReviewAsync(string owner, string repo, int prNumber, string language, CancellationToken cancellationToken = default) { var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + var useSubAgent = _config.AgentFramework?.UseSubAgent ?? true; if (useFunctionCalling) { - var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( - owner, repo, prNumber, language, cancellationToken); - - if (actionResult != null) + if (useSubAgent) { - _logger.LogInformation( - "Review completed. Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", - actionResult.LineCommentsPosted, - actionResult.ReviewCommentsPosted, - actionResult.Approved); + var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( + owner, repo, prNumber, language, cancellationToken); + + if (actionResult != null) + { + _logger.LogInformation( + "Review completed (with SubAgent). Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", + actionResult.LineCommentsPosted, + actionResult.ReviewCommentsPosted, + actionResult.Approved); + } + + return reviewText; + } + else + { + var (reviewText, actionResult) = await _reviewAgent.ReviewDirectAsync( + owner, repo, prNumber, language, cancellationToken); + + if (actionResult != null) + { + _logger.LogInformation( + "Review completed (direct mode). Line comments: {LineComments}, Review comments: {ReviewComments}, Approved: {Approved}", + actionResult.LineCommentsPosted, + actionResult.ReviewCommentsPosted, + actionResult.Approved); + } + + return reviewText; } - - return reviewText; } return await _reviewAgent.ReviewAsync(owner, repo, prNumber, cancellationToken: cancellationToken); @@ -88,7 +129,7 @@ public async Task ReviewAsync(string owner, string repo, int prNumber, s /// /// レビューと承認を一連のワークフローとして実行します - /// ReviewAgentに統合されたため、ReviewWithLineCommentsAsyncを使用 + /// ReviewAgentに統合されたため、ReviewWithLineCommentsAsyncまたはReviewDirectAsyncを使用 /// public async Task ReviewAndApproveAsync( string owner, @@ -98,11 +139,13 @@ public async Task ReviewAndApproveAsync( CancellationToken cancellationToken = default) { var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + var useSubAgent = _config.AgentFramework?.UseSubAgent ?? true; if (useFunctionCalling) { - var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( - owner, repo, prNumber, language: null, cancellationToken); + var (reviewText, actionResult) = useSubAgent + ? await _reviewAgent.ReviewWithLineCommentsAsync(owner, repo, prNumber, language: null, cancellationToken) + : await _reviewAgent.ReviewDirectAsync(owner, repo, prNumber, language: null, cancellationToken); return new ApprovalResult { @@ -135,11 +178,13 @@ public async Task ReviewAndApproveAsync( CancellationToken cancellationToken = default) { var useFunctionCalling = _config.AgentFramework?.EnableFunctionCalling ?? false; + var useSubAgent = _config.AgentFramework?.UseSubAgent ?? true; if (useFunctionCalling) { - var (reviewText, actionResult) = await _reviewAgent.ReviewWithLineCommentsAsync( - owner, repo, prNumber, language, cancellationToken); + var (reviewText, actionResult) = useSubAgent + ? await _reviewAgent.ReviewWithLineCommentsAsync(owner, repo, prNumber, language, cancellationToken) + : await _reviewAgent.ReviewDirectAsync(owner, repo, prNumber, language, cancellationToken); return new ApprovalResult {