< Summary

Information
Class: Orchestrator.Commands.Observability.AnalyzeMatch.AnalyzeMatchDetailedCommand.RunMetric
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/AnalyzeMatch/AnalyzeMatchDetailedCommand.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 267
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_RunNumber()100%210%
set_RunNumber(...)100%210%
get_Duration()100%210%
set_Duration(...)100%210%
get_Success()100%210%
set_Success(...)100%210%
get_Cost()100%210%
set_Cost(...)100%210%
.ctor(...)100%210%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/AnalyzeMatch/AnalyzeMatchDetailedCommand.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics;
 4using System.Globalization;
 5using System.Linq;
 6using System.Threading.Tasks;
 7using EHonda.KicktippAi.Core;
 8using Microsoft.Extensions.Logging;
 9using Spectre.Console;
 10using Spectre.Console.Cli;
 11using Spectre.Console.Rendering;
 12using OpenAiIntegration;
 13using Orchestrator.Commands.Shared;
 14using Orchestrator.Infrastructure.Factories;
 15
 16namespace Orchestrator.Commands.Observability.AnalyzeMatch;
 17
 18public class AnalyzeMatchDetailedCommand : AsyncCommand<AnalyzeMatchDetailedSettings>
 19{
 20    private readonly IAnsiConsole _console;
 21    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 22    private readonly IKicktippClientFactory _kicktippClientFactory;
 23    private readonly IOpenAiServiceFactory _openAiServiceFactory;
 24
 25    public AnalyzeMatchDetailedCommand(
 26        IAnsiConsole console,
 27        IFirebaseServiceFactory firebaseServiceFactory,
 28        IKicktippClientFactory kicktippClientFactory,
 29        IOpenAiServiceFactory openAiServiceFactory)
 30    {
 31        _console = console;
 32        _firebaseServiceFactory = firebaseServiceFactory;
 33        _kicktippClientFactory = kicktippClientFactory;
 34        _openAiServiceFactory = openAiServiceFactory;
 35    }
 36
 37    public override async Task<int> ExecuteAsync(CommandContext context, AnalyzeMatchDetailedSettings settings)
 38    {
 39        var loggerFactory = AnalyzeMatchCommandHelpers.CreateLoggerFactory(settings.Debug);
 40        var logger = loggerFactory.CreateLogger<AnalyzeMatchDetailedCommand>();
 41
 42        try
 43        {
 44            var validation = settings.Validate();
 45            if (!validation.Successful)
 46            {
 47                _console.MarkupLine($"[red]Error:[/] {validation.Message}");
 48                return 1;
 49            }
 50
 51            var predictionService = _openAiServiceFactory.CreatePredictionService(settings.Model);
 52            var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 53            var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 54            var kicktippClient = _kicktippClientFactory.CreateClient();
 55
 56            var communityContext = settings.CommunityContext!;
 57
 58            _console.MarkupLine($"[green]Analyze match initialized with model:[/] [yellow]{settings.Model}[/]");
 59            _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 60            _console.MarkupLine($"[blue]Runs:[/] [yellow]{settings.Runs}[/]");
 61
 62            if (settings.Debug)
 63            {
 64                _console.MarkupLine("[dim]Debug logging enabled[/]");
 65            }
 66
 67            var match = await AnalyzeMatchCommandHelpers.ResolveMatchAsync(settings, kicktippClient, logger, communityCo
 68            if (match == null)
 69            {
 70                _console.MarkupLine("[red]Failed to resolve match details. Aborting.[/]");
 71                return 1;
 72            }
 73
 74            var contextDocuments = new List<DocumentContext>();
 75
 76            if (contextRepository != null)
 77            {
 78                var contextDocumentInfos = await AnalyzeMatchCommandHelpers.GetMatchContextDocumentsAsync(
 79                    contextRepository,
 80                    match.HomeTeam,
 81                    match.AwayTeam,
 82                    communityContext,
 83                    settings.Verbose);
 84
 85                contextDocuments = contextDocumentInfos.Select(info => info.Document).ToList();
 86
 87                if (contextDocumentInfos.Any())
 88                {
 89                    _console.MarkupLine("[dim]Loaded context documents:[/]");
 90                    foreach (var info in contextDocumentInfos)
 91                    {
 92                        _console.MarkupLine($"[grey]  • {info.Document.Name}[/] [dim](v{info.Version})[/]");
 93
 94                        if (settings.ShowContextDocuments)
 95                        {
 96                            var lines = info.Document.Content.Split('\n');
 97                            foreach (var line in lines.Take(10))
 98                            {
 99                                _console.MarkupLine($"[grey]      {line.EscapeMarkup()}[/]");
 100                            }
 101
 102                            if (lines.Length > 10)
 103                            {
 104                                _console.MarkupLine($"[dim]      ... ({lines.Length - 10} more lines) ...[/]");
 105                            }
 106                        }
 107                    }
 108                }
 109                else
 110                {
 111                    _console.MarkupLine("[yellow]No context documents retrieved; proceeding without additional context[/
 112                }
 113            }
 114            else
 115            {
 116                _console.MarkupLine("[yellow]Context repository not configured. Proceeding without context documents.[/]
 117            }
 118
 119            tokenUsageTracker.Reset();
 120
 121            var predictions = new List<Prediction>();
 122            var runMetrics = new List<RunMetric>();
 123            var enableLiveEstimates = !settings.NoLiveEstimates;
 124
 125            string FormatCurrencyValue(decimal value) => $"${value.ToString("F4", CultureInfo.InvariantCulture)}";
 126            string FormatCurrencyOptional(decimal? value) => value.HasValue ? FormatCurrencyValue(value.Value) : "n/a";
 127
 128            string FormatDurationValue(TimeSpan value) => value.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCultur
 129            string FormatDurationOptional(TimeSpan? value) => value.HasValue ? FormatDurationValue(value.Value) : "n/a";
 130
 131            IRenderable BuildSummary()
 132            {
 133                var completedRuns = runMetrics.Count;
 134                var remainingRuns = Math.Max(settings.Runs - completedRuns, 0);
 135                var successfulRuns = runMetrics.Where(metric => metric.Success).ToList();
 136                var costValues = successfulRuns
 137                    .Where(metric => metric.Cost.HasValue)
 138                    .Select(metric => metric.Cost!.Value)
 139                    .ToList();
 140
 141                var totalCostSoFar = costValues.Aggregate(0m, (sum, value) => sum + value);
 142                decimal? averageCost = costValues.Count > 0 ? totalCostSoFar / costValues.Count : (decimal?)null;
 143                decimal? projectedCost = averageCost.HasValue ? averageCost.Value * settings.Runs : (decimal?)null;
 144
 145                var totalDuration = runMetrics.Aggregate(TimeSpan.Zero, (current, metric) => current + metric.Duration);
 146                TimeSpan? averageDuration = runMetrics.Count > 0
 147                    ? TimeSpan.FromTicks(totalDuration.Ticks / runMetrics.Count)
 148                    : (TimeSpan?)null;
 149                TimeSpan? estimatedRemaining = averageDuration.HasValue && remainingRuns > 0
 150                    ? TimeSpan.FromTicks(averageDuration.Value.Ticks * remainingRuns)
 151                    : (TimeSpan?)null;
 152
 153                var table = new Table()
 154                    .Title("[bold yellow]Live Estimates[/]")
 155                    .Border(TableBorder.Rounded)
 156                    .AddColumn(new TableColumn("[grey]Metric[/]").LeftAligned())
 157                    .AddColumn(new TableColumn("[grey]Value[/]").LeftAligned());
 158
 159                table.AddRow("Completed runs", $"{completedRuns}/{settings.Runs}");
 160                table.AddRow("Successful predictions", $"{successfulRuns.Count}/{settings.Runs}");
 161                table.AddRow("Total cost so far", FormatCurrencyValue(totalCostSoFar));
 162                table.AddRow("Average cost", FormatCurrencyOptional(averageCost));
 163                table.AddRow("Projected total cost", FormatCurrencyOptional(projectedCost));
 164                table.AddRow("Average run time", FormatDurationOptional(averageDuration));
 165                table.AddRow("Estimated remaining time", FormatDurationOptional(estimatedRemaining));
 166                table.AddRow("Elapsed time", FormatDurationValue(totalDuration));
 167
 168                return table;
 169            }
 170
 171            Action refreshSummary = () => { };
 172
 173            async Task ExecuteRunsAsync()
 174            {
 175                for (var run = 1; run <= settings.Runs; run++)
 176                {
 177                    var stopwatch = Stopwatch.StartNew();
 178
 179                    _console.MarkupLine($"[cyan]\nRun {run}/{settings.Runs}[/]");
 180
 181                    var prediction = await predictionService.PredictMatchAsync(
 182                        match,
 183                        contextDocuments,
 184                        includeJustification: true);
 185
 186                    stopwatch.Stop();
 187
 188                    if (prediction == null)
 189                    {
 190                        _console.MarkupLine("[red]  ✗ Prediction failed[/]");
 191                        runMetrics.Add(new RunMetric(run, stopwatch.Elapsed, false, null));
 192                        refreshSummary();
 193                        continue;
 194                    }
 195
 196                    predictions.Add(prediction);
 197
 198                    var lastCost = tokenUsageTracker.GetLastCost();
 199                    var usageSummary = tokenUsageTracker.GetLastUsageCompactSummary();
 200
 201                    runMetrics.Add(new RunMetric(run, stopwatch.Elapsed, true, lastCost));
 202
 203                    _console.MarkupLine($"[green]  ✓ Prediction:[/] [yellow]{prediction.HomeGoals}:{prediction.AwayGoals
 204
 205                    var justificationWriter = new JustificationConsoleWriter(_console);
 206                    justificationWriter.WriteJustification(
 207                        prediction.Justification,
 208                        "[cyan]  ↳ Justification:[/]",
 209                        "      ",
 210                        "[yellow]  ↳ Justification: no explanation returned by model[/]");
 211
 212                    _console.MarkupLine($"[magenta]  ↳ Cost:[/] [cyan]{FormatCurrencyValue(lastCost)}[/] [grey]({usageSu
 213
 214                    refreshSummary();
 215                }
 216
 217                refreshSummary();
 218            }
 219
 220            if (enableLiveEstimates)
 221            {
 222                await _console.Live(BuildSummary())
 223                    .AutoClear(false)
 224                    .StartAsync(async ctx =>
 225                    {
 226                        refreshSummary = () =>
 227                        {
 228                            ctx.UpdateTarget(BuildSummary());
 229                            ctx.Refresh();
 230                        };
 231
 232                        refreshSummary();
 233                        await ExecuteRunsAsync();
 234                    });
 235            }
 236            else
 237            {
 238                await ExecuteRunsAsync();
 239                _console.Write(BuildSummary());
 240            }
 241
 242            if (predictions.Any())
 243            {
 244                _console.MarkupLine($"\n[blue]Total runs with predictions:[/] [yellow]{predictions.Count}/{settings.Runs
 245                _console.MarkupLine($"[blue]Total cost:[/] [yellow]{FormatCurrencyValue(tokenUsageTracker.GetTotalCost()
 246            }
 247            else
 248            {
 249                _console.MarkupLine("[red]No successful predictions generated.[/]");
 250            }
 251
 252            return 0;
 253        }
 254        catch (Exception ex)
 255        {
 256            logger.LogError(ex, "Error executing analyze-match command");
 257            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 258            return 1;
 259        }
 260        finally
 261        {
 262            loggerFactory.Dispose();
 263        }
 264    }
 265
 0266    private sealed record RunMetric(int RunNumber, TimeSpan Duration, bool Success, decimal? Cost);
 267}