diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/ChatScript.cs b/src/FlowPair/Agent/Operations/ReviewChanges/ChatScript.cs new file mode 100644 index 0000000..876bd64 --- /dev/null +++ b/src/FlowPair/Agent/Operations/ReviewChanges/ChatScript.cs @@ -0,0 +1,93 @@ +using System.Collections.Immutable; + +namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges; + +public sealed record ChatScript( + string Name, + ImmutableArray Extensions, + string SystemInstruction, + ImmutableList Instructions) +{ + public const string StopKeywordPlaceholder = ""; + public static readonly ImmutableList Default = + [ + new( + "Generic programming chat script", + [ + /* Python */".py", ".pyw", ".pyx", ".pxd", ".pxi", + /* JavaScript */".js", ".jsx", ".mjs", ".cjs", + /* Java */".java", + /* C# */".cs", ".csx", + /* C++ */".cpp", ".cxx", ".cc", ".c++", ".hpp", ".hxx", ".h", ".hh", ".h++", + /* PHP */".php", ".phtml", ".phps", + /* Ruby */".rb", ".rbw", ".rake", + /* Swift */".swift", + /* R */".r", + /* SQL */".sql", + /* Kotlin */".kt", ".kts", + /* TypeScript */".ts", ".tsx", + /* Go (Golang) */".go", + /* Rust */".rs", + /* Scala */".scala", ".sc", + /* Dart */".dart", + /* Perl */".pl", ".pm", ".t", ".pod", + /* MATLAB */".m", + /* VBA */".bas", ".cls", ".frm", + /* Shell Scripting */".sh", ".bash", ".zsh", ".ksh", ".csh", ".tcsh", ".fish", + ], + """ + You are an expert developer, your task is to review a set of changes on Git commits. + You are given a set of Git patches, containing the filenames and their partial contents. Note that you might not have the full context of the code. + Only review lines of code which have been changed (added or removed) in the pull request. Other lines are added to provide context but should be ignored in the review. + Begin your feedback by evaluating the changed code using a risk score similar to a LOGAF score but measured from 0 to 3, where 0 is the lowest risk to the codebase if the code is merged and 3 is the highest risk which would likely break something or be unsafe. Risk score should be described as "0 - Not important", "1 - Low priority adjustments", "2 - Medium priority adjustments" or "3 - High priority adjustments". + Only provide feedback on critical issues. If the code is already well-written or issues are minor, do not provide any feedback. + Avoid commenting on breaking functions down into smaller, more manageable functions unless it is a significant problem. Be aware that there will be libraries and techniques used which you might not be familiar with, so do not comment on those unless you are confident that there is a problem. + """, + [ + Instruction.MultiStepInstruction.Of( + "Give feedback to ", + [ + "improve readability where it can significantly impacts understanding", + "make code cleaner where it introduces substantial benefits", + "maximize the performance of the code where there is a clear, impactful improvement", + "flag any API keys or secrets present in plain text immediately as highest risk", + "rate the changes based on SOLID principles", + "apply the principles of DRY, KISS, YAGNI and Clean Code", + "avoid magic strings and numbers", + "ensure new code follow existing patterns and structure", + ], + $" if applicable; otherwise, reply with \"{StopKeywordPlaceholder}\" when there are no suggestions"), + Instruction.StepInstruction.Of( + """ + Ensure the feedback contain the file path and the line number. + Do not provide positive reinforcement or comments on good decisions. Focus solely on areas that need improvement. + """), + Instruction.StepInstruction.Of( + """ + Ensure the feedback details are brief, concise, and accurate. If there are multiple similar issues, only comment on the most critical. + Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. + Use the same programming language as the file under review. If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes. + """), + Instruction.StepInstruction.Of( + """ + Ensure the message in the feedback is in English. + Ensure the feedback do not infer unknown code, do not speculate the referenced code. + """), + Instruction.JsonConvertInstruction.Of( + """ + Format the feedback in a valid JSON format as a list of feedbacks, or "[]" for no feedbacks. + The "feedback" property can be multiline and include example code snippets. + The schema of the JSON feedback object must be: + """), + ]), + ]; + + public static Option FindChatScriptForFile( + IReadOnlyList scripts, + string filePath) + { + return scripts + .Reverse() + .FirstOrDefault(i => i.Extensions.Any(s => filePath.EndsWith(s, StringComparison.OrdinalIgnoreCase))); + } +} diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/ChatThreadSpec.cs b/src/FlowPair/Agent/Operations/ReviewChanges/ChatThreadSpec.cs new file mode 100644 index 0000000..3e22b47 --- /dev/null +++ b/src/FlowPair/Agent/Operations/ReviewChanges/ChatThreadSpec.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; +using Ciandt.FlowTools.FlowPair.Flow.Operations.ProxyCompleteChat.v1; + +namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges; + +public static class ChatThreadSpec +{ + public static bool IsClosed(ImmutableList thread, string stopKeyword) => + thread[^1].Role == Role.Assistant && + thread[^1].Content.Contains(stopKeyword, StringComparison.Ordinal); +} diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/ContentDeserializer.cs b/src/FlowPair/Agent/Operations/ReviewChanges/ContentDeserializer.cs index 11a473e..6099444 100644 --- a/src/FlowPair/Agent/Operations/ReviewChanges/ContentDeserializer.cs +++ b/src/FlowPair/Agent/Operations/ReviewChanges/ContentDeserializer.cs @@ -7,33 +7,35 @@ namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges; public static class ContentDeserializer { - public static ImmutableList TryDeserializeFeedback( + public static Result, string> TryDeserializeFeedback( ReadOnlySpan content, JsonTypeInfo> typeInfo) { if (content.IsWhiteSpace()) - return []; + return "JSON not found on empty content"; + JsonException? lastException = null; while (!content.IsEmpty) { var start = content.IndexOf('['); if (start < 0) - return []; + return "Invalid JSON: '[' not found"; - var end = content.IndexOf(']'); + var end = content.LastIndexOf(']'); if (end < start) - return []; + return "Invalid JSON: ']' not found or comes before '['"; try { return JsonSerializer.Deserialize(content[start..(end + 1)], typeInfo) ?? []; } - catch (JsonException) + catch (JsonException exception) { - content = content[(end + 1)..]; + lastException = exception; + content = content[(start + 1)..]; } } - return []; + return lastException?.Message ?? "Invalid JSON"; } } diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.cs b/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.cs index 62a822c..da8bbb9 100644 --- a/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.cs +++ b/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.cs @@ -58,7 +58,7 @@ public override string TransformText() #line hidden this.Write(":"); - this.Write(this.ToStringHelper.ToStringWithCulture(response.Line)); + this.Write(this.ToStringHelper.ToStringWithCulture(response.LineRange)); #line default #line hidden diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.tt b/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.tt index adab680..efbd03a 100644 --- a/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.tt +++ b/src/FlowPair/Agent/Operations/ReviewChanges/FeedbackHtmlTemplate.tt @@ -28,7 +28,7 @@

Risk Score: <#= response.RiskScore #> <#= response.RiskDescription #>

-

File: <#= response.Path #>:<#= response.Line #>

+

File: <#= response.Path #>:<#= response.LineRange #>

<#= HtmlFormatter.EncodeToHtml(response.Feedback) #>
diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/Instructions.cs b/src/FlowPair/Agent/Operations/ReviewChanges/Instructions.cs index b820c01..1b614d3 100644 --- a/src/FlowPair/Agent/Operations/ReviewChanges/Instructions.cs +++ b/src/FlowPair/Agent/Operations/ReviewChanges/Instructions.cs @@ -1,109 +1,14 @@ using System.Collections.Immutable; +using FxKit.CompilerServices; namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges; -public sealed record Instructions( - string Name, - ImmutableArray Extensions, - string Message) +[Union] +public partial record Instruction { - public static readonly ImmutableList Default = - [ - new Instructions( - "Generic programming instructions", - [ - /* Python */".py", ".pyw", ".pyx", ".pxd", ".pxi", - /* JavaScript */".js", ".jsx", ".mjs", ".cjs", - /* Java */".java", - /* C# */".cs", ".csx", - /* C++ */".cpp", ".cxx", ".cc", ".c++", ".hpp", ".hxx", ".h", ".hh", ".h++", - /* PHP */".php", ".phtml", ".phps", - /* Ruby */".rb", ".rbw", ".rake", - /* Swift */".swift", - /* R */".r", - /* SQL */".sql", - /* Kotlin */".kt", ".kts", - /* TypeScript */".ts", ".tsx", - /* Go (Golang) */".go", - /* Rust */".rs", - /* Scala */".scala", ".sc", - /* Dart */".dart", - /* Perl */".pl", ".pm", ".t", ".pod", - /* MATLAB */".m", - /* VBA */".bas", ".cls", ".frm", - /* Shell Scripting */".sh", ".bash", ".zsh", ".ksh", ".csh", ".tcsh", ".fish" - ], - """ - You are an expert developer, your task is to review a set of pull requests on Azure DevOps. + partial record StepInstruction(string Messsage); - You are given a set of Git patches, containing the filenames and their partial contents. Note that you might not have the full context of the code. + partial record MultiStepInstruction(string Preamble, ImmutableList Messages, string Ending); - Only review lines of code which have been changed (added or removed) in the pull request. Lines which have been removed have the type `REMOVED` and lines which have been added have the type `ADDED`. Other lines are added to provide context but should be ignored in the review. - - Begin your feedback by evaluating the changed code using a risk score similar to a LOGAF score but measured from 0 to 3, where 0 is the lowest risk to the codebase if the code is merged and 3 is the highest risk which would likely break something or be unsafe. Risk score should be described as "0 - Not important", "1 - Low priority adjustments", "2 - Medium priority adjustments" or "3 - High priority adjustments". - - In your feedback: - 1. Focus exclusively on highlighting potential bugs. - 2. Improve readability only if it significantly impacts understanding. - 3. Make code cleaner only if it introduces substantial benefits. - 4. Maximize the performance of the code if there is a clear, impactful improvement. - 5. Flag any API keys or secrets present in plain text immediately as highest risk. - 6. Rate the changes based on SOLID principles if applicable. - 7. Apply the principles of DRY, KISS, YAGNI and Clean Code during the review of the code. - 8. Do not infer unknown code, do not speculate the referenced code. - 9. Avoid magic strings and numbers. - 10. New code should follow existing patterns and structure. - - Only provide feedback on critical issues. If the code is already well-written or issues are minor, do not provide any feedback. - - Avoid commenting on breaking functions down into smaller, more manageable functions unless it is a significant problem. Be aware that there will be libraries and techniques used which you might not be familiar with, so do not comment on those unless you are confident that there is a problem. - - Do not provide positive reinforcement or comments on good decisions. Focus solely on areas that need improvement. - - Your feedbacks will be input in Azure DevOps via API `/comments` endpoint. The feedbacks should be in a valid JSON format. - - Use markdown formatting for the feedback details. Do not include the filename or risk level in the feedback details. - - Ensure the feedback details are brief, concise, and accurate. If there are multiple similar issues, only comment on the most critical. - - Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review. If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes. - - It is not necessary to add low-risk comments that are not relevant to changes in the pull request. - - The message in the field text must be in English. - - Format the response in a valid JSON format as a list of feedbacks. Remember it is crucial that the result has the file path. - This valid JSON is going to be inserted in a value of a key-value from another JSON object, be-aware about the formatting. Remember to only list feedbacks that needs user action. - The schema of the JSON feedback object must be: - ```json - [ - { - "riskScore": 0, - "riskDescription": "Not important", - "feedback": "", - "path": "/path/path/file.extension", - "line": 16, - "lineType": "ADDED" - }, - { - "riskScore": 1, - "riskDescription": "Low priority adjustments", - "feedback": "", - "path": "/path/path/file.extension", - "line": 20, - "lineType": "ADDED" - } - ] - ``` - """) - ]; - - public static Option FindInstructionsForFile( - IReadOnlyList instructions, - string filePath) - { - return instructions - .Reverse() - .FirstOrDefault(i => i.Extensions.Any(s => filePath.EndsWith(s, StringComparison.OrdinalIgnoreCase))); - } + partial record JsonConvertInstruction(string Message); } diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/ReviewChangesCommand.cs b/src/FlowPair/Agent/Operations/ReviewChanges/ReviewChangesCommand.cs index 9676679..d6a123c 100644 --- a/src/FlowPair/Agent/Operations/ReviewChanges/ReviewChangesCommand.cs +++ b/src/FlowPair/Agent/Operations/ReviewChanges/ReviewChangesCommand.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Globalization; using System.IO.Abstractions; using System.Text; using Ciandt.FlowTools.FlowPair.Agent.Infrastructure; @@ -27,11 +28,13 @@ public class ReviewChangesCommand( /// Review changed files using Flow. /// /// Path to the repository. + /// -c, Commit hash [Command("review")] public int Execute( - [Argument] string? path = null) + [Argument] string? path = null, + string? commit = null) { - return (from diff in getChangesHandler.Extract(path).OkOr(0) + return (from diff in getChangesHandler.Extract(path, commit).OkOr(0) from session in loginUseCase.Execute(isBackground: true) .UnwrapErrOr(0) .Ensure(n => n == 0, 1) @@ -43,10 +46,12 @@ from feedback in BuildFeedback(diff) private Result BuildFeedback(ImmutableList changes) { var feedback = changes - .GroupBy(c => Instructions.FindInstructionsForFile(Instructions.Default, c.Path)) + .GroupBy(c => ChatScript.FindChatScriptForFile(ChatScript.Default, c.Path)) .Where(g => g.Key.IsSome) - .Select(g => new { Instructions = g.Key.Unwrap(), Diff = g.AggregateToStringLines(c => c.Diff) }) - .SelectMany(x => GetFeedback(AllowedModel.Claude35Sonnet, x.Diff, x.Instructions)) + .Select(g => new { Script = g.Key.Unwrap(), Diff = g.AggregateToStringLines(c => c.Diff) }) + .SelectMany( + x => console.Progress() + .Start(c => GetFeedback(c, AllowedModel.Claude35Sonnet, x.Diff, x.Script))) .Where(f => !string.IsNullOrWhiteSpace(f.Feedback)) .OrderByDescending(x => x.RiskScore).ThenBy(x => x.Path, StringComparer.OrdinalIgnoreCase) .ToImmutableList(); @@ -70,23 +75,178 @@ private Result BuildFeedback(ImmutableList changes) } private ImmutableList GetFeedback( + ProgressContext progressContext, AllowedModel model, string diff, - Instructions instructions) + ChatScript chatScript) { - var result = completeChatHandler.ChatCompletion( - model, - [new Message(Role.System, instructions.Message), new Message(Role.User, diff)]); + var progress = progressContext.AddTask( + $"Running '{chatScript.Name}'", + maxValue: CalculateTotalSteps(chatScript)); + ImmutableList> chatThreads = + [ + [new Message(Role.System, chatScript.SystemInstruction), new Message(Role.User, diff)], + ]; - if (!result.IsOk) + var stopKeyword = $"<{Guid.NewGuid().ToString("N")[..8]}>"; + foreach (var instruction in chatScript.Instructions) { - console.MarkupLineInterpolated($"[bold red]Error:[/] {result.UnwrapErr().ToString()}"); - return []; + var currThreads = chatThreads; + var runResult = instruction.Match( + StepInstruction: x => RunInstruction(progress, model, stopKeyword, x, currThreads), + MultiStepInstruction: x => RunInstruction(progress, model, stopKeyword, x, currThreads), + JsonConvertInstruction: x => RunInstruction(progress, model, stopKeyword, x, currThreads)); + if (!runResult.IsSome) + { + return []; + } + + chatThreads = runResult.Unwrap(); } - var feedback = ContentDeserializer.TryDeserializeFeedback( - result.Unwrap().Content, - jsonContext.ImmutableListReviewerFeedbackResponse); + var feedback = MergeFeedback(chatThreads); return feedback; } + + private static double CalculateTotalSteps(ChatScript chatScript) => + chatScript.Instructions + .Aggregate( + (IEnumerable) [0D], + (curr, next) => next.Match( + StepInstruction: _ => curr.Select(v => v + 1), + MultiStepInstruction: x => Enumerable.Range(0, x.Messages.Count).Select(_ => curr.First() + 1), + JsonConvertInstruction: _ => curr.Select(v => v + 1))) + .Sum(); + + private Option>> RunInstruction( + ProgressTask progress, + AllowedModel model, + string stopKeyword, + Instruction.StepInstruction instruction, + ImmutableList> chatThreads) + { + progress.Increment(chatThreads.Count(x => ChatThreadSpec.IsClosed(x, stopKeyword))); + + var newMessage = new Message( + Role.User, + instruction.Messsage.Replace(ChatScript.StopKeywordPlaceholder, stopKeyword)); + + return (from chatThread in chatThreads.AsParallel() + where !ChatThreadSpec.IsClosed(chatThread, stopKeyword) + select CompleteChat(model, chatThread.Add(newMessage)) + .DoAlways(() => progress.Increment(1))) + .Sequence() + .Map(l => l.Concat(chatThreads.Where(x => ChatThreadSpec.IsClosed(x, stopKeyword)))) + .Map(l => l.ToImmutableList()); + } + + private Option>> RunInstruction( + ProgressTask progress, + AllowedModel model, + string stopKeyword, + Instruction.MultiStepInstruction instruction, + ImmutableList> chatThreads) + { + if (chatThreads.Count > 1) + { + console.MarkupLine("[bold red]Error:[/] Only one multi-step instruction is supported."); + return None; + } + + if (ChatThreadSpec.IsClosed(chatThreads[0], stopKeyword)) + { + return chatThreads; + } + + var existingThread = chatThreads[0]; + var template = string.Format( + CultureInfo.InvariantCulture, + "{0}{{0}}{1}", + instruction.Preamble.Replace(ChatScript.StopKeywordPlaceholder, stopKeyword), + instruction.Ending.Replace(ChatScript.StopKeywordPlaceholder, stopKeyword)); + + return instruction.Messages.AsParallel() + .Select( + msg => CompleteChat( + model, + existingThread.Add( + new Message( + Role.User, + string.Format( + CultureInfo.InvariantCulture, + template, + msg.Replace(ChatScript.StopKeywordPlaceholder, stopKeyword))))) + .DoAlways(() => progress.Increment(1))) + .Sequence() + .Map(l => l.ToImmutableList()); + } + + private Option>> RunInstruction( + ProgressTask progress, + AllowedModel model, + string stopKeyword, + Instruction.JsonConvertInstruction instruction, + ImmutableList> chatThreads) + { + progress.Increment(chatThreads.Count(x => ChatThreadSpec.IsClosed(x, stopKeyword))); + + var newMessage = new Message(Role.User, $"{instruction.Message}\n\n{JsonSchema.FeedbackJsonSchema}"); + return chatThreads + .Where(t => !ChatThreadSpec.IsClosed(t, stopKeyword)) + .AsParallel() + .Select( + t => + { + if (!CompleteChat(model, t.Add(newMessage)).TryGet(out var newThread)) + { + progress.Increment(1); + return None; + } + + var deserializeResult = ContentDeserializer.TryDeserializeFeedback( + newThread[^1].Content, + jsonContext.ImmutableListReviewerFeedbackResponse); + while (!deserializeResult.TryGet(out _, out var error)) + { + if (CompleteChat(model, newThread.Add(new Message(Role.User, error))) + .TryGet(out newThread)) + { + progress.Increment(1); + return None; + } + + deserializeResult = ContentDeserializer.TryDeserializeFeedback( + newThread![^1].Content, + jsonContext.ImmutableListReviewerFeedbackResponse); + } + + progress.Increment(1); + return Some(newThread); + }) + .Sequence() + .Map(l => l.Concat(chatThreads.Where(x => ChatThreadSpec.IsClosed(x, stopKeyword)))) + .Map(l => l.ToImmutableList()); + } + + private Option> CompleteChat(AllowedModel model, ImmutableList chatThread) + { + return completeChatHandler.ChatCompletion(model, chatThread) + .Match( + msg => Some(chatThread.Add(msg)), + error => + { + console.MarkupLineInterpolated($"[bold red]Error:[/] {error.ToString()}"); + return None; + }); + } + + private ImmutableList MergeFeedback(ImmutableList> chatThreads) + { + return chatThreads + .SelectMany( + t => ContentDeserializer + .TryDeserializeFeedback(t[^1].Content, jsonContext.ImmutableListReviewerFeedbackResponse) + .UnwrapOr([])) + .ToImmutableList(); + } } diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/v1/JsonSchema.cs b/src/FlowPair/Agent/Operations/ReviewChanges/v1/JsonSchema.cs new file mode 100644 index 0000000..bc3381b --- /dev/null +++ b/src/FlowPair/Agent/Operations/ReviewChanges/v1/JsonSchema.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1; + +public static class JsonSchema +{ + [StringSyntax(StringSyntaxAttribute.Json)] + public const string FeedbackJsonSchema = + """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "riskScore": { + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + "riskDescription": { + "type": "string", + "enum": [ + "Not important", + "Low priority adjustments", + "Medium priority adjustments", + "High priority adjustments" + ] + }, + "feedback": { + "type": "string", + "minLength": 5 + }, + "path": { + "type": "string", + "pattern": "^/.*" + }, + "lineRange": { + "type": "string", + "pattern": "^\\d+-\\d+$" + } + }, + "required": ["riskScore", "riskDescription", "feedback", "path", "lineRange"], + "additionalProperties": false + } + } + """; +} diff --git a/src/FlowPair/Agent/Operations/ReviewChanges/v1/ReviewerFeedbackResponse.cs b/src/FlowPair/Agent/Operations/ReviewChanges/v1/ReviewerFeedbackResponse.cs index cdeaf9e..eb05289 100644 --- a/src/FlowPair/Agent/Operations/ReviewChanges/v1/ReviewerFeedbackResponse.cs +++ b/src/FlowPair/Agent/Operations/ReviewChanges/v1/ReviewerFeedbackResponse.cs @@ -5,5 +5,4 @@ public sealed record ReviewerFeedbackResponse( string RiskDescription, string Feedback, string Path, - int Line, - string LineType); + string LineRange); diff --git a/src/FlowPair/Common/FunctionalExtensions.cs b/src/FlowPair/Common/FunctionalExtensions.cs index ba359f9..ad4b2dd 100644 --- a/src/FlowPair/Common/FunctionalExtensions.cs +++ b/src/FlowPair/Common/FunctionalExtensions.cs @@ -5,6 +5,13 @@ namespace Ciandt.FlowTools.FlowPair.Common; public static class FunctionalExtensions { + public static Option DoAlways(this Option source, Action action) + where T : notnull + { + action(); + return source; + } + /// /// Returns a failure result if the predicate is false. Otherwise, returns a result with the specified value. /// diff --git a/src/FlowPair/Flow/Operations/AnthropicCompleteChat/v1/AnthropicChatCompletionRequest.cs b/src/FlowPair/Flow/Operations/AnthropicCompleteChat/v1/AnthropicChatCompletionRequest.cs index be0d34f..71077b9 100644 --- a/src/FlowPair/Flow/Operations/AnthropicCompleteChat/v1/AnthropicChatCompletionRequest.cs +++ b/src/FlowPair/Flow/Operations/AnthropicCompleteChat/v1/AnthropicChatCompletionRequest.cs @@ -12,10 +12,13 @@ namespace Ciandt.FlowTools.FlowPair.Flow.Operations.AnthropicCompleteChat.v1; /// The anthropic version. /// The amount of randomness injected into the response, between 0 and 1. /// The maximum number of tokens to generate before stopping. +/// +/// See Anthropic Claude Messages API. +/// public sealed record AnthropicChatCompletionRequest( [property: JsonPropertyName("allowedModels")] ImmutableList AllowedModels, [property: JsonPropertyName("messages")] ImmutableList Messages, [property: JsonPropertyName("system")] string? System = null, [property: JsonPropertyName("anthropic_version")] string AnthropicVersion = "bedrock-2023-05-31", [property: JsonPropertyName("temperature")] float? Temperature = null, - [property: JsonPropertyName("max_tokens")] int MaxTokens = 4096); + [property: JsonPropertyName("max_tokens")] int MaxTokens = 8192); diff --git a/src/FlowPair/Git/GetChanges/GitGetChangesHandler.cs b/src/FlowPair/Git/GetChanges/GitGetChangesHandler.cs index 8223728..8fa69a2 100644 --- a/src/FlowPair/Git/GetChanges/GitGetChangesHandler.cs +++ b/src/FlowPair/Git/GetChanges/GitGetChangesHandler.cs @@ -15,7 +15,7 @@ public class GitGetChangesHandler( IAnsiConsole console) : IGitGetChangesHandler { - public Option> Extract(string? path) + public Option> Extract(string? path, string? commit) { path = TryFindRepository(fileSystem, path).UnwrapOrNull(); if (path is null) @@ -27,6 +27,46 @@ public Option> Extract(string? path) using var repo = new Repository(path); var builder = ImmutableList.CreateBuilder(); + if (!string.IsNullOrEmpty(commit)) + { + if (!FillChangesFromCommit(repo, builder, commit)) + { + return None; + } + } + else + { + FillChangesFallback(repo, builder); + } + + console.MarkupLine($"Found {builder.Count} changed files"); + return Some(builder.ToImmutable()); + } + + private bool FillChangesFromCommit(Repository repo, ImmutableList.Builder builder, string commit) + { + var foundCommit = repo.Commits + .FirstOrDefault(c => c.Sha.StartsWith(commit, StringComparison.OrdinalIgnoreCase)); + if (foundCommit is null) + { + console.MarkupLine("[red]Error:[/] Could not locate specified commit."); + return false; + } + + var parentCommit = foundCommit.Parents.FirstOrDefault(); + + foreach (var changes in repo.Diff + .Compare(parentCommit?.Tree, foundCommit.Tree) + .Where(p => !p.IsBinaryComparison && p.Status != ChangeKind.Deleted && p.Mode != Mode.Directory)) + { + builder.Add(new FileChange(changes.Path, changes.Patch)); + } + + return true; + } + + private static void FillChangesFallback(Repository repo, ImmutableList.Builder builder) + { FillChanges(repo, builder, DiffTargets.Index); if (builder.Count == 0) @@ -38,9 +78,6 @@ public Option> Extract(string? path) { FillChangesFromLastCommit(repo, builder); } - - console.MarkupLine($"Found {builder.Count} changed files"); - return Some(builder.ToImmutable()); } private static void FillChanges(Repository repo, ImmutableList.Builder builder, DiffTargets diffTargets)