Skip to content

Commit

Permalink
feat(agent): Create repository context services (#26)
Browse files Browse the repository at this point in the history
- Update FxKit to 0.8.4 (support TryAggregate)
- Create DirectoryStructureMessageFactory to build a message with repository structure
- Create ProjectFilesMessageFactory to build a message with the content of project key files
- Fix ReadAllText(To) extension methods for using abstractions
  • Loading branch information
skarllot authored Jan 3, 2025
1 parent 13f575e commit bb3ee24
Show file tree
Hide file tree
Showing 21 changed files with 827 additions and 125 deletions.
7 changes: 5 additions & 2 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
<ItemGroup>
<PackageVersion Include="AutomaticInterface" Version="5.0.3" />
<PackageVersion Include="ConsoleAppFramework" Version="5.3.3" />
<PackageVersion Include="FxKit" Version="0.8.3" />
<PackageVersion Include="FxKit.CompilerServices" Version="0.8.3" />
<PackageVersion Include="FxKit" Version="0.8.4" />
<PackageVersion Include="FxKit.CompilerServices" Version="0.8.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Jab" Version="0.10.2" />
<PackageVersion Include="LibGit2Sharp" Version="0.31.0" />
<PackageVersion Include="Raiqub.Generators.EnumUtilities" Version="1.9.21" />
Expand Down
5 changes: 5 additions & 0 deletions src/FlowPair/Agent/Infrastructure/IAgentModule.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ciandt.FlowTools.FlowPair.Agent.Operations.Login;
using Ciandt.FlowTools.FlowPair.Agent.Operations.ReviewChanges;
using Ciandt.FlowTools.FlowPair.Agent.Services;
using Jab;

namespace Ciandt.FlowTools.FlowPair.Agent.Infrastructure;
Expand All @@ -9,6 +10,10 @@ namespace Ciandt.FlowTools.FlowPair.Agent.Infrastructure;
// Infrastructure
[Singleton(typeof(AgentJsonContext), Factory = nameof(GetJsonContext))]

// Services
[Singleton(typeof(IDirectoryStructureMessageFactory), typeof(DirectoryStructureMessageFactory))]
[Singleton(typeof(IProjectFilesMessageFactory), typeof(ProjectFilesMessageFactory))]

// Chat definitions
[Singleton(typeof(IReviewChatDefinition), typeof(ReviewChatDefinition))]

Expand Down
26 changes: 26 additions & 0 deletions src/FlowPair/Agent/Services/DirectoryStructureMessageFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.IO.Abstractions;
using AutomaticInterface;
using Ciandt.FlowTools.FlowPair.Chats.Models;
using Ciandt.FlowTools.FlowPair.LocalFileSystem.GetDirectoryStructure;

namespace Ciandt.FlowTools.FlowPair.Agent.Services;

public partial interface IDirectoryStructureMessageFactory;

[GenerateAutomaticInterface]
public sealed class DirectoryStructureMessageFactory(
IGetDirectoryStructureHandler getDirectoryStructureHandler)
: IDirectoryStructureMessageFactory
{
public Message CreateWithRepositoryStructure(IDirectoryInfo directory)
{
return new Message(
SenderRole.User,
$"""
The repository has the following directory structure:
```
{getDirectoryStructureHandler.Execute(directory)}
```
""");
}
}
37 changes: 37 additions & 0 deletions src/FlowPair/Agent/Services/FileNaming.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Immutable;

namespace Ciandt.FlowTools.FlowPair.Agent.Services;

public static class FileNaming
{
public static ImmutableList<string> ProjectExtensions { get; } =
[
".csproj", ".slnx", // C#
".Rproj", // R
".xcodeproj", ".xcworkspace", // Swift
".project", // Java (Eclipse)
".workspace", // C++ (CodeBlocks)
".idea", // Kotlin, Scala (IntelliJ IDEA)
".prj", // MATLAB
];

public static ImmutableList<string> ProjectFiles { get; } =
[
"Directory.Packages.props", "Directory.Build.props", "Directory.Build.targets", // C#
"pom.xml", "build.gradle", // Java (Maven, Gradle)
"pyproject.toml", "setup.py", // Python
"package.json", // JavaScript
"CMakeLists.txt", "Makefile", // C++
"composer.json", // PHP
"Gemfile", // Ruby
"Package.swift", // Swift
"DESCRIPTION", // R
"build.gradle.kts", // Kotlin
"tsconfig.json", // TypeScript
"go.mod", // Go (Golang)
"Cargo.toml", // Rust
"build.sbt", // Scala
"pubspec.yaml", // Dart
"Makefile.PL", "dist.ini", // Perl
];
}
50 changes: 50 additions & 0 deletions src/FlowPair/Agent/Services/ProjectFilesMessageFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.IO.Abstractions;
using System.Text;
using AutomaticInterface;
using Ciandt.FlowTools.FlowPair.Chats.Models;
using Ciandt.FlowTools.FlowPair.Common;
using Ciandt.FlowTools.FlowPair.LocalFileSystem.Services;

namespace Ciandt.FlowTools.FlowPair.Agent.Services;

public partial interface IProjectFilesMessageFactory;

[GenerateAutomaticInterface]
public sealed class ProjectFilesMessageFactory(
IWorkingDirectoryWalker workingDirectoryWalker)
: IProjectFilesMessageFactory
{
public Message CreateWithProjectFilesContent(
IDirectoryInfo rootDirectory,
IEnumerable<string>? extensions = null,
IEnumerable<string>? filenames = null)
{
return new Message(
SenderRole.User,
workingDirectoryWalker
.FindFilesByExtension(rootDirectory, extensions ?? FileNaming.ProjectExtensions)
.Concat(workingDirectoryWalker.FindFilesByName(rootDirectory, filenames ?? FileNaming.ProjectFiles))
.Aggregate(new StringBuilder(), (curr, next) => AggregateFileContent(curr, next, rootDirectory))
.ToString());
}

private static StringBuilder AggregateFileContent(
StringBuilder sb,
IFileInfo fileInfo,
IDirectoryInfo rootDirectory)
{
if (sb.Length > 0)
{
sb.AppendLine();
}

sb.Append("File: ");
sb.Append(rootDirectory.GetRelativePath(fileInfo.FullName));
sb.AppendLine();
sb.AppendLine("```");
fileInfo.ReadAllTextTo(sb);
sb.AppendLine();
sb.AppendLine("```");
return sb;
}
}
1 change: 0 additions & 1 deletion src/FlowPair/Chats/Models/ChatThread.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.Immutable;
using Ciandt.FlowTools.FlowPair.Chats.Services;
using Ciandt.FlowTools.FlowPair.Common;
using Ciandt.FlowTools.FlowPair.Flow.Operations.ProxyCompleteChat;
using Spectre.Console;

Expand Down
1 change: 0 additions & 1 deletion src/FlowPair/Chats/Services/ChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Ciandt.FlowTools.FlowPair.Chats.Contracts.v1;
using Ciandt.FlowTools.FlowPair.Chats.Infrastructure;
using Ciandt.FlowTools.FlowPair.Chats.Models;
using Ciandt.FlowTools.FlowPair.Common;
using Ciandt.FlowTools.FlowPair.Flow.Operations.ProxyCompleteChat;
using Ciandt.FlowTools.FlowPair.LocalFileSystem.Services;
using Spectre.Console;
Expand Down
11 changes: 8 additions & 3 deletions src/FlowPair/Common/FileSystemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public static class FileSystemExtensions
{
private const int DefaultBufferSize = 1024;

public static string GetRelativePath(this IDirectoryInfo directory, string path)
{
return directory.FileSystem.Path.GetRelativePath(directory.FullName, path);
}

public static IFileInfo NewFile(this IDirectoryInfo directoryInfo, string fileName)
{
var fileSystem = directoryInfo.FileSystem;
Expand All @@ -28,13 +33,13 @@ public static void WriteAllText(this IFileInfo fileInfo, string? contents, Encod

public static string ReadAllText(this IFileInfo fileInfo)
{
using var reader = new StreamReader(fileInfo.FullName, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
return reader.ReadToEnd();
return fileInfo.FileSystem.File.ReadAllText(fileInfo.FullName);
}

public static void ReadAllTextTo(this IFileInfo fileInfo, StringBuilder sb)
{
using var reader = new StreamReader(fileInfo.FullName, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
using var stream = fileInfo.OpenRead();
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);

var buffer = ArrayPool<char>.Shared.Rent(DefaultBufferSize);
try
Expand Down
22 changes: 0 additions & 22 deletions src/FlowPair/Common/FunctionalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,6 @@ namespace Ciandt.FlowTools.FlowPair.Common;

public static class FunctionalExtensions
{
public static Result<TAccumulate, TError> TryAggregate<TSource, TAccumulate, TError>(
this IEnumerable<TSource> source,
TAccumulate seed,
[InstantHandle] Func<TAccumulate, TSource, Result<TAccumulate, TError>> func)
where TAccumulate : notnull
where TError : notnull
{
var result = Ok<TAccumulate, TError>(seed);

foreach (var item in source)
{
if (!result.TryGet(out var value, out var error))
{
return error;
}

result = func(value, item);
}

return result;
}

/// <summary>
/// Returns a failure result if the predicate is false. Otherwise, returns a result with the specified value.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions src/FlowPair/LocalFileSystem/Services/PathAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text.RegularExpressions;

namespace Ciandt.FlowTools.FlowPair.LocalFileSystem.Services;

public static partial class PathAnalyzer
{
public static string Normalize(string path)
{
if (OperatingSystem.IsWindows())
{
return path.Replace('/', '\\');
}
else
{
var tmp = path.AsSpan();
if (DriveLetterRegex().IsMatch(tmp))
{
tmp = tmp[2..];

if (tmp.IsEmpty)
return "/";
}

return string.Create(
length: tmp.Length,
state: tmp,
action: static (dest, src) => src.Replace(dest, '\\', '/'));
}
}

[GeneratedRegex("^[a-zA-Z]:")]
private static partial Regex DriveLetterRegex();
}
20 changes: 20 additions & 0 deletions src/FlowPair/LocalFileSystem/Services/WorkingDirectoryWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ public sealed class WorkingDirectoryWalker(
IFileSystem fileSystem)
: IWorkingDirectoryWalker
{
private static readonly EnumerationOptions s_fileEnumerationOptions = new()
{
IgnoreInaccessible = true,
MatchCasing = MatchCasing.CaseInsensitive,
MatchType = MatchType.Simple,
RecurseSubdirectories = true
};

public Option<IDirectoryInfo> TryFindRepositoryRoot(string? path)
{
var currentDirectory = fileSystem.DirectoryInfo.New(path ?? fileSystem.Directory.GetCurrentDirectory());
Expand All @@ -32,4 +40,16 @@ public Option<IDirectoryInfo> TryFindRepositoryRoot(string? path)

return None;
}

public IEnumerable<IFileInfo> FindFilesByExtension(IDirectoryInfo rootDirectory, IEnumerable<string> extensions)
{
return extensions
.SelectMany(e => rootDirectory.EnumerateFiles($"*{e}", s_fileEnumerationOptions));
}

public IEnumerable<IFileInfo> FindFilesByName(IDirectoryInfo rootDirectory, IEnumerable<string> filenames)
{
return filenames
.SelectMany(e => rootDirectory.EnumerateFiles(e, s_fileEnumerationOptions));
}
}
18 changes: 9 additions & 9 deletions src/FlowPair/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@
},
"FxKit": {
"type": "Direct",
"requested": "[0.8.3, )",
"resolved": "0.8.3",
"contentHash": "4Ka+j3jf2Pjb1n0L30MjiMdHd6i5tI0lKppvKKi4OqM0uO0s4s8yZUYJH4eUiaiNVY9aHPBFuUVcbOX+bfEvfg==",
"requested": "[0.8.4, )",
"resolved": "0.8.4",
"contentHash": "rzPMlTzQNRNYs/5eXLHC7Qx3UI+1k9X8oaSBASUi6L/Y6lFw81PA7jrxxqSX6ebhtIs0wplq/EagrTBna3+v9g==",
"dependencies": {
"FxKit.CompilerServices.Annotations": "0.8.3",
"FxKit.CompilerServices.Annotations": "0.8.4",
"JetBrains.Annotations": "2024.3.0"
}
},
"FxKit.CompilerServices": {
"type": "Direct",
"requested": "[0.8.3, )",
"resolved": "0.8.3",
"contentHash": "0YBpWO3ds1jxAr61cNWxLmaNWRVvoW52vJ9fZLyvpcOFp6PbWwpppcndRxVB03odgWloW4jkrAv8rQIqpUBLKA=="
"requested": "[0.8.4, )",
"resolved": "0.8.4",
"contentHash": "LlMM1NBZ9Pwkog93IMxZjTkQngphR4IDTC86PDv9P1grg6VgnFCo/rY+yMibCd7KDZ09AYfj7onieZLym+mz5A=="
},
"Jab": {
"type": "Direct",
Expand Down Expand Up @@ -92,8 +92,8 @@
},
"FxKit.CompilerServices.Annotations": {
"type": "Transitive",
"resolved": "0.8.3",
"contentHash": "VhTqB4b0G/pxPcSNJ/iR0TLK+63i3UgrJ/CZ/8VX/oN0Ck1/WdsucxUqE6wJAHEOG3LNB9yxh8MA4cHfF20o1w=="
"resolved": "0.8.4",
"contentHash": "L6IcCB4wRURS0TfUjXElNfLSQBK6sg7OLJJTStPvivJdwAMP/GNhIYbdxMel4s9Klnx9pCVh3C73XC5hwW966A=="
},
"JetBrains.Annotations": {
"type": "Transitive",
Expand Down
2 changes: 1 addition & 1 deletion tests/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PackageVersion>
<PackageVersion Include="coverlet.msbuild" Version="6.0.3" />
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
<PackageVersion Include="FxKit.Testing" Version="0.8.3" />
<PackageVersion Include="FxKit.Testing" Version="0.8.4" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.IO.Abstractions;
using Ciandt.FlowTools.FlowPair.Agent.Services;
using Ciandt.FlowTools.FlowPair.Chats.Models;
using Ciandt.FlowTools.FlowPair.LocalFileSystem.GetDirectoryStructure;
using FluentAssertions;
using JetBrains.Annotations;
using NSubstitute;

namespace Ciandt.FlowTools.FlowPair.Tests.Agent.Services;

[TestSubject(typeof(DirectoryStructureMessageFactory))]
public class DirectoryStructureMessageFactoryTest
{
private readonly IGetDirectoryStructureHandler _getDirectoryStructureHandler;
private readonly DirectoryStructureMessageFactory _factory;

public DirectoryStructureMessageFactoryTest()
{
_getDirectoryStructureHandler = Substitute.For<IGetDirectoryStructureHandler>();
_factory = new DirectoryStructureMessageFactory(_getDirectoryStructureHandler);
}

[Fact]
public void CreateWithRepositoryStructureShouldReturnCorrectMessage()
{
// Arrange
var mockDirectory = Substitute.For<IDirectoryInfo>();
const string expectedStructure = "mock/directory/structure";
_getDirectoryStructureHandler
.Execute(mockDirectory)
.Returns(expectedStructure);

// Act
var result = _factory.CreateWithRepositoryStructure(mockDirectory);

// Assert
result.Should().NotBeNull();
result.Role.Should().Be(SenderRole.User);
result.Content.Should().Contain("The repository has the following directory structure:");
result.Content.Should().Contain("```");
result.Content.Should().Contain(expectedStructure);
_getDirectoryStructureHandler.Received(1).Execute(mockDirectory);
}

[Fact]
public void CreateWithRepositoryStructureShouldHandleEmptyStructure()
{
// Arrange
var mockDirectory = Substitute.For<IDirectoryInfo>();
_getDirectoryStructureHandler
.Execute(mockDirectory)
.Returns(string.Empty);

// Act
var result = _factory.CreateWithRepositoryStructure(mockDirectory);

// Assert
result.Should().NotBeNull();
result.Role.Should().Be(SenderRole.User);
result.Content.Should().Contain("The repository has the following directory structure:");
result.Content.Should().Contain("```");
}
}
Loading

0 comments on commit bb3ee24

Please sign in to comment.