Skip to content

Commit

Permalink
Implement a CLI (#4)
Browse files Browse the repository at this point in the history
- Support configure, login and review commands
- Reorganize classes
- Simplify dependency tree
  • Loading branch information
skarllot authored Dec 22, 2024
1 parent ffeda98 commit a49f2c5
Show file tree
Hide file tree
Showing 72 changed files with 894 additions and 630 deletions.
6 changes: 6 additions & 0 deletions .idea/.idea.flow-pair/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<PackageOutputPath>$(SolutionDir)artifacts</PackageOutputPath>
<PublishDir>$(SolutionDir)publish</PublishDir>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<LangVersion>13</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
Expand Down
2 changes: 2 additions & 0 deletions flow-pair.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=ciandt/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
4 changes: 2 additions & 2 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
</ItemGroup>
<ItemGroup>
<PackageVersion Include="AutomaticInterface" Version="5.0.3" />
<PackageVersion Include="ConsoleAppFramework" Version="5.3.1" />
<PackageVersion Include="FxKit" Version="0.8.3" />
<PackageVersion Include="FxKit.CompilerServices" Version="0.8.3" />
<PackageVersion Include="Jab" Version="0.10.2" />
<PackageVersion Include="LibGit2Sharp" Version="0.31.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageVersion Include="Raiqub.Generators.EnumUtilities" Version="1.9.21" />
<PackageVersion Include="Raiqub.Generators.T4CodeWriter" Version="1.0.64" />
<PackageVersion Include="Raiqub.Generators.T4CodeWriter.Sources" Version="1.0.64" />
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<PackageVersion Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.1.7" />
</ItemGroup>
Expand Down
14 changes: 14 additions & 0 deletions src/FlowPair/Agent/Infrastructure/AgentJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1;

namespace Ciandt.FlowTools.FlowPair.Agent.Infrastructure;

[JsonSourceGenerationOptions(
GenerationMode = JsonSourceGenerationMode.Default,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
RespectNullableAnnotations = true)]
[JsonSerializable(typeof(ImmutableList<ReviewerFeedbackResponse>))]
public partial class AgentJsonContext : JsonSerializerContext;
19 changes: 19 additions & 0 deletions src/FlowPair/Agent/Infrastructure/IAgentModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Ciandt.FlowTools.FlowPair.Agent.Operations.Login;
using Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges;
using Jab;

namespace Ciandt.FlowTools.FlowPair.Agent.Infrastructure;

[ServiceProviderModule]

// Infrastructure
[Singleton(typeof(AgentJsonContext), Factory = nameof(GetJsonContext))]

// Operations
[Singleton(typeof(ILoginUseCase), typeof(LoginUseCase))]
[Singleton(typeof(LoginCommand))]
[Singleton(typeof(ReviewChangesCommand))]
public interface IAgentModule
{
static AgentJsonContext GetJsonContext() => AgentJsonContext.Default;
}
18 changes: 18 additions & 0 deletions src/FlowPair/Agent/Operations/Login/LoginCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Ciandt.FlowTools.FlowPair.Common;
using ConsoleAppFramework;

namespace Ciandt.FlowTools.FlowPair.Agent.Operations.Login;

public class LoginCommand(
ILoginUseCase loginUseCase)
{
/// <summary>
/// Sign in to the Flow.
/// </summary>
[Command("login")]
public int Execute()
{
return loginUseCase.Execute(isBackground: false)
.UnwrapErrOr(0);
}
}
93 changes: 93 additions & 0 deletions src/FlowPair/Agent/Operations/Login/LoginUseCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using AutomaticInterface;
using Ciandt.FlowTools.FlowPair.Common;
using Ciandt.FlowTools.FlowPair.Flow;
using Ciandt.FlowTools.FlowPair.Flow.Contracts;
using Ciandt.FlowTools.FlowPair.Flow.Infrastructure;
using Ciandt.FlowTools.FlowPair.Flow.Operations.GenerateToken;
using Ciandt.FlowTools.FlowPair.Settings.Contracts.v1;
using Ciandt.FlowTools.FlowPair.Settings.Services;
using Ciandt.FlowTools.FlowPair.Support.Persistence;
using Ciandt.FlowTools.FlowPair.UserSessions.Contracts.v1;
using Ciandt.FlowTools.FlowPair.UserSessions.Services;
using Spectre.Console;

namespace Ciandt.FlowTools.FlowPair.Agent.Operations.Login;

public partial interface ILoginUseCase;

[GenerateAutomaticInterface]
public sealed class LoginUseCase(
TimeProvider timeProvider,
IAnsiConsole console,
FlowHttpClient httpClient,
IAppSettingsRepository appSettingsRepository,
IUserSessionRepository userSessionRepository,
IFlowGenerateTokenHandler generateTokenHandler)
: ILoginUseCase
{
public Result<UserSession, int> Execute(bool isBackground)
{
var result = from config in appSettingsRepository.Read().MapErr(HandleConfigurationError)
from session in userSessionRepository.Read()
.Do(SetupHttpAuthentication)
.UnwrapOrElse(UserSession.Empty)
.Ensure(s => s.IsExpired(timeProvider), 0)
from auth in RequestToken(config, session, verbose: isBackground).MapErr(HandleFlowError)
.Do(s => userSessionRepository.Save(s))
select auth;

if (!isBackground)
{
result.Do(s => console.WriteLine($"Signed in with expiration at {s.ExpiresAt:g}"));
}

return result;
}

private int HandleConfigurationError(GetJsonFileValueError error)
{
var errorMessage = error.Match<FormattableString>(
NotFound: _ => $"[red]Error:[/] Configuration not found.",
Invalid: x => $"[red]Error:[/] {x.Exception.Message}",
Null: _ => $"[red]Error:[/] Configuration is null.",
UnknownVersion: x => $"[red]Error:[/] The configuration version '{x.Version}' is not supported.");

console.MarkupLineInterpolated(errorMessage);
return 1;
}

private void SetupHttpAuthentication(UserSession session)
{
if (session.IsExpired(timeProvider))
return;

httpClient.BearerToken = session.AccessToken;
}

private Result<UserSession, FlowError> RequestToken(
AppConfiguration configuration,
UserSession userSession,
bool verbose)
{
if (verbose)
{
console.Write("Signing in to Flow...");
}

var result = generateTokenHandler.Execute(configuration, userSession);
if (verbose)
{
result
.Do(_ => console.WriteLine(" OK"))
.DoErr(_ => console.WriteLine(" FAIL"));
}

return result;
}

private int HandleFlowError(FlowError error)
{
console.MarkupLineInterpolated($"[red]Error:[/] {error.FullMessage}");
return 2;
}
}
39 changes: 39 additions & 0 deletions src/FlowPair/Agent/Operations/ReviewChanges/ContentDeserializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1;

namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges;

public static class ContentDeserializer
{
public static ImmutableList<ReviewerFeedbackResponse> TryDeserializeFeedback(
ReadOnlySpan<char> content,
JsonTypeInfo<ImmutableList<ReviewerFeedbackResponse>> typeInfo)
{
if (content.IsWhiteSpace())
return [];

while (!content.IsEmpty)
{
var start = content.IndexOf('[');
if (start < 0)
return [];

var end = content.IndexOf(']');
if (end < start)
return [];

try
{
return JsonSerializer.Deserialize(content[start..(end + 1)], typeInfo) ?? [];
}
catch (JsonException)
{
content = content[(end + 1)..];
}
}

return [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
namespace Ciandt.FlowTools.FlowPair.Agent.ReviewChanges
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges
{
using System.Collections.Immutable;
using System.Text;
using Ciandt.FlowTools.FlowPair.Agent.ReviewChanges.v1;
using Ciandt.FlowTools.FlowPair.Common;
using Raiqub.Generators.T4CodeWriter;
using Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1;
using System;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<#@ template language="C#" debug="false" linePragmas="false" hostspecific="false" inherits="CodeWriterBase<ImmutableList<ReviewerFeedbackResponse>>" #>
<#@ import namespace="System.Collections.Immutable" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="Ciandt.FlowTools.FlowPair.Agent.ReviewChanges.v1" #>
<#@ import namespace="Ciandt.FlowTools.FlowPair.Common" #>
<#@ import namespace="Raiqub.Generators.T4CodeWriter" #>
<#@ import namespace="Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1" #>
<!DOCTYPE html>
<html>
<head>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Immutable;

namespace Ciandt.FlowTools.FlowPair.Agent.ReviewChanges;
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges;

public sealed record Instructions(
string Name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Immutable;
using System.IO.Abstractions;
using System.Text;
using Ciandt.FlowTools.FlowPair.Agent.Infrastructure;
using Ciandt.FlowTools.FlowPair.Agent.Operations.Login;
using Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1;
using Ciandt.FlowTools.FlowPair.ChangeTracking;
using Ciandt.FlowTools.FlowPair.Common;
using Ciandt.FlowTools.FlowPair.Flow.Operations.ProxyCompleteChat;
using Ciandt.FlowTools.FlowPair.Flow.Operations.ProxyCompleteChat.v1;
using Ciandt.FlowTools.FlowPair.Support.Persistence;
using Ciandt.FlowTools.FlowPair.Support.Presentation;
using ConsoleAppFramework;
using Spectre.Console;

namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges;

public class ReviewChangesCommand(
IAnsiConsole console,
IFileSystem fileSystem,
AgentJsonContext jsonContext,
IGitDiffExtractor gitDiffExtractor,
ILoginUseCase loginUseCase,
IProxyCompleteChatHandler completeChatHandler)
{
/// <summary>
/// Review changed files using Flow.
/// </summary>
[Command("review")]
public int Execute()
{
return (from diff in gitDiffExtractor.Extract().OkOr(0)
from session in loginUseCase.Execute(isBackground: true)
.UnwrapErrOr(0)
.Ensure(n => n == 0, 1)
from feedback in BuildFeedback(diff)
select 0)
.UnwrapEither();
}

private Result<Unit, int> BuildFeedback(ImmutableList<FileChange> changes)
{
var feedback = changes
.GroupBy(c => Instructions.FindInstructionsForFile(Instructions.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))
.Where(f => !string.IsNullOrWhiteSpace(f.Feedback))
.OrderByDescending(x => x.RiskScore).ThenBy(x => x.Path, StringComparer.OrdinalIgnoreCase)
.ToImmutableList();

console.WriteLine($"Created {feedback.Count} comments");

if (feedback.Count > 0)
{
var tempPath = ApplicationData.GetTempPath(fileSystem);
tempPath.Create();

var feedbackFilePath = tempPath.NewFile($"{DateTime.UtcNow:yyyyMMddHHmmss}-feedback.html");

var htmlContent = new FeedbackHtmlTemplate(feedback).TransformText();
feedbackFilePath.WriteAllText(htmlContent, Encoding.UTF8);

FileLauncher.OpenFile(feedbackFilePath.FullName);
}

return Unit();
}

private ImmutableList<ReviewerFeedbackResponse> GetFeedback(
AllowedModel model,
string diff,
Instructions instructions)
{
var result = completeChatHandler.ChatCompletion(
model,
[new Message(Role.System, instructions.Message), new Message(Role.User, diff)]);

if (!result.IsOk)
{
console.MarkupLineInterpolated($"[bold red]Error:[/] {result.UnwrapErr().ToString()}");
return [];
}

var feedback = ContentDeserializer.TryDeserializeFeedback(
result.Unwrap().Content,
jsonContext.ImmutableListReviewerFeedbackResponse);
return feedback;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Ciandt.FlowTools.FlowPair.Agent.ReviewChanges.v1;
namespace Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges.v1;

public sealed record ReviewerFeedbackResponse(
int RiskScore,
Expand Down
Loading

0 comments on commit a49f2c5

Please sign in to comment.