Skip to content

Commit

Permalink
feat(chat): Show estimated remaining time to run chat script (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
skarllot authored Jan 9, 2025
1 parent 8daa3d1 commit 0618b9b
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 13 deletions.
18 changes: 7 additions & 11 deletions src/FlowPair/Agent/Operations/Login/LoginUseCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,16 @@ private Result<UserSession, FlowError> RequestToken(
UserSession userSession,
bool verbose)
{
if (verbose)
if (!verbose)
{
console.Write("Signing in to Flow...");
return generateTokenHandler.Execute(configuration, userSession);
}

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

return result;
return console.Status()
.Start(
"Generating access token",
_ => generateTokenHandler.Execute(configuration, userSession))
.Do(_ => console.WriteLine("New access token generated"));
}

private int HandleFlowError(FlowError error)
Expand Down
11 changes: 9 additions & 2 deletions src/FlowPair/Chats/Services/ChatService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Raiqub.LlmTools.FlowPair.Chats.Models;
using Raiqub.LlmTools.FlowPair.Flow.Operations.ProxyCompleteChat;
using Raiqub.LlmTools.FlowPair.LocalFileSystem.Services;
using Raiqub.LlmTools.FlowPair.Support.Console;
using Spectre.Console;

namespace Raiqub.LlmTools.FlowPair.Chats.Services;
Expand All @@ -25,8 +26,14 @@ public Result<TResult, string> Run<TResult>(
IReadOnlyList<Message> initialMessages)
where TResult : notnull
{
return progress.Start(
context => RunInternal(context, llmModelType, chatDefinition, initialMessages));
return progress
.Columns(
new SpinnerColumn(Spinner.Known.Star),
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new LongRemainingTimeColumn())
.Start(context => RunInternal(context, llmModelType, chatDefinition, initialMessages));
}

private Result<TResult, string> RunInternal<TResult>(
Expand Down
82 changes: 82 additions & 0 deletions src/FlowPair/Support/Console/LongRemainingTimeColumn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Spectre.Console;
using Spectre.Console.Rendering;

namespace Raiqub.LlmTools.FlowPair.Support.Console;

public sealed class LongRemainingTimeColumn : ProgressColumn
{
private readonly TimeProvider _timeProvider;
private double _lastStep;
private DateTime? _estimatedFinishTime;

public LongRemainingTimeColumn(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}

/// <inheritdoc/>
protected override bool NoWrap => true;

/// <summary>
/// Gets or sets the style of the remaining time text.
/// </summary>
public Style? Style { get; set; } = Color.Blue;

/// <inheritdoc/>
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
{
var now = _timeProvider.GetLocalNow();
CollectSample(task, now);

var remaining = GetRemainingTime(task, now);
if (remaining == null)
{
return new Markup("--:--:--");
}

if (remaining.Value.TotalHours > 99)
{
return new Markup("**:**:**");
}

return new Text($"{remaining.Value:hh\\:mm\\:ss}", Style ?? Style.Plain);
}

/// <inheritdoc/>
public override int? GetColumnWidth(RenderOptions options) => 8;

private void CollectSample(ProgressTask task, DateTimeOffset now)
{
if (task.StartTime == null || task.Value.Equals(_lastStep))
{
return;
}

var currTime = now.DateTime;
var timeElapsed = currTime - task.StartTime.Value;
var rateSecs = timeElapsed.TotalSeconds / task.Value;
var remainingSteps = task.MaxValue - task.Value;
var remainingTime = remainingSteps * rateSecs;

_lastStep = task.Value;
_estimatedFinishTime = currTime + TimeSpan.FromSeconds(remainingTime);
}

private TimeSpan? GetRemainingTime(ProgressTask task, DateTimeOffset now)
{
if (task.IsFinished)
{
return TimeSpan.Zero;
}

if (_estimatedFinishTime == null)
{
return null;
}

var currTime = now.DateTime;
return currTime < _estimatedFinishTime.Value
? _estimatedFinishTime.Value - currTime
: null;
}
}
169 changes: 169 additions & 0 deletions tests/FlowPair.Tests/Support/Console/LongRemainingTimeColumnTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using FluentAssertions;
using NSubstitute;
using Raiqub.LlmTools.FlowPair.Support.Console;
using Raiqub.LlmTools.FlowPair.Tests.Testing;
using Spectre.Console;
using Spectre.Console.Rendering;
using Spectre.Console.Testing;

namespace Raiqub.LlmTools.FlowPair.Tests.Support.Console;

public sealed class LongRemainingTimeColumnTests : IDisposable
{
private readonly TimeProvider _timeProvider = Substitute.For<TimeProvider>();
private readonly TestConsole _testConsole = new();
private readonly RenderOptions _renderOptions;

public LongRemainingTimeColumnTests()
{
_renderOptions = RenderOptions.Create(_testConsole);
_timeProvider.LocalTimeZone.Returns(TimeZoneInfo.Utc);
}

public void Dispose() => _testConsole.Dispose();

[Fact]
public void ConstructorWithNullTimeProviderShouldUseSystemTimeProvider()
{
var column = new LongRemainingTimeColumn();
column.Should().NotBeNull();
var result = column.Render(
options: _renderOptions,
task: new ProgressTask(1, "Test", 100),
deltaTime: TimeSpan.Zero);

result.GetText().Should().Be("--:--:--");
}

[Fact]
public void PropertiesShouldHaveCorrectDefaultValues()
{
var column = new LongRemainingTimeColumn();

column.Style.Should().Be((Style)Color.Blue);
column.GetColumnWidth(_renderOptions).Should().Be(8);
var noWrap = column.GetType()
.GetProperty(
"NoWrap",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.GetValue(column);
noWrap.Should().Be(true);
}

[Theory]
[InlineData(null)]
[InlineData(0)]
[InlineData(100)] // Task finished
public void RenderSpecialCasesShouldReturnExpectedOutput(int? value)
{
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(startTime.AddMinutes(10)));

var column = new LongRemainingTimeColumn(_timeProvider);
var task = new ProgressTask(1, "Test", 100, autoStart: false) { Value = value ?? 0 };
task.Unsafe().StartTime = startTime;

var result = column.Render(_renderOptions, task, TimeSpan.Zero);

if (value == 100)
result.GetText().Should().Be("00:00:00");
else
result.GetText().Should().Be("--:--:--");
}

[Theory]
[InlineData(25, 5, "00:15:00")]
[InlineData(50, 10, "00:10:00")]
[InlineData(75, 15, "00:05:00")]
public void RenderTaskInProgressShouldReturnAccurateFormattedTime(
int value,
int elapsedMinutes,
string expected)
{
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var currentTime = startTime.AddMinutes(elapsedMinutes);
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(currentTime));

var column = new LongRemainingTimeColumn(_timeProvider);
var task = new ProgressTask(1, "Test", 100) { Value = value };
task.Unsafe().StartTime = startTime;

var result = column.Render(_renderOptions, task, TimeSpan.Zero);

result.GetText().Should().Be(expected);
}

[Fact]
public void RenderWhenRemainingTimeExceeds99HoursShouldReturnAsterisks()
{
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var currentTime = startTime.AddHours(1);
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(currentTime));

var column = new LongRemainingTimeColumn(_timeProvider);
var task = new ProgressTask(1, "Test", 10000) { Value = 1 };
task.Unsafe().StartTime = startTime;

var result = column.Render(_renderOptions, task, TimeSpan.Zero);

result.GetText().Should().Be("**:**:**");
}

[Fact]
public void RenderEstimationBehaviorShouldBeCorrect()
{
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var currentTime = startTime.AddMinutes(5);
_timeProvider.GetUtcNow().Returns(
new DateTimeOffset(currentTime),
new DateTimeOffset(currentTime),
new DateTimeOffset(currentTime.AddMinutes(20)),
new DateTimeOffset(currentTime.AddMinutes(200)));

var column = new LongRemainingTimeColumn(_timeProvider);
var task = new ProgressTask(1, "Test", 100) { Value = 25 };
task.Unsafe().StartTime = startTime;

// First render to set the estimated finish time
var firstResult = column.Render(_renderOptions, task, TimeSpan.Zero);

// Second render with unchanged task value
var secondResult = column.Render(_renderOptions, task, TimeSpan.Zero);
secondResult.GetText().Should().Be(firstResult.GetText());

// Third render after the estimated finish time
var thirdResult = column.Render(_renderOptions, task, TimeSpan.Zero);
thirdResult.GetText().Should().Be("--:--:--");

// Update task value and render again
task.Value = 50;
var fourthResult = column.Render(_renderOptions, task, TimeSpan.Zero);
fourthResult.GetText().Should().NotBe(secondResult.GetText());
}

[Fact]
public void RenderWithCustomAndNullStylesShouldUseCorrectStyle()
{
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var currentTime = startTime.AddMinutes(5);
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(currentTime));

var task = new ProgressTask(1, "Test", 100) { Value = 25 };
task.Unsafe().StartTime = startTime;

// Custom style
var customStyle = new Style(foreground: Color.Red);
var columnWithCustomStyle = new LongRemainingTimeColumn(_timeProvider) { Style = customStyle };
var customStyleResult = columnWithCustomStyle.Render(_renderOptions, task, TimeSpan.Zero);
customStyleResult.Should().NotBeNull();
customStyleResult.Render(_renderOptions, int.MaxValue).Should()
.AllSatisfy(s => s.Style.Should().Be(customStyle));

// Null style
var columnWithNullStyle = new LongRemainingTimeColumn(_timeProvider) { Style = null };
var nullStyleResult = columnWithNullStyle.Render(_renderOptions, task, TimeSpan.Zero);
nullStyleResult.Should().NotBeNull();
nullStyleResult.Render(_renderOptions, int.MaxValue).Should()
.AllSatisfy(s => s.Style.Should().Be(Style.Plain));
}
}
Loading

0 comments on commit 0618b9b

Please sign in to comment.