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