< Summary

Information
Class: Orchestrator.Commands.Observability.AnalyzeMatch.AnalyzeMatchComparisonCommand.ComparisonRunResult
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/AnalyzeMatch/AnalyzeMatchComparisonCommand.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 264
Line coverage: 100%
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%11100%
get_RunNumber()100%210%
set_RunNumber(...)100%210%
get_IncludeJustification()100%11100%
set_IncludeJustification(...)100%210%
get_Prediction()100%11100%
set_Prediction(...)100%210%
get_Duration()100%11100%
set_Duration(...)100%210%
get_Cost()100%11100%
set_Cost(...)100%210%
get_Success()100%11100%
set_Success(...)100%210%
.ctor(...)100%210%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/AnalyzeMatch/AnalyzeMatchComparisonCommand.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 OpenAiIntegration;
 12using Orchestrator.Infrastructure.Factories;
 13
 14namespace Orchestrator.Commands.Observability.AnalyzeMatch;
 15
 16public class AnalyzeMatchComparisonCommand : AsyncCommand<AnalyzeMatchComparisonSettings>
 17{
 18    private readonly IAnsiConsole _console;
 19    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 20    private readonly IKicktippClientFactory _kicktippClientFactory;
 21    private readonly IOpenAiServiceFactory _openAiServiceFactory;
 22
 23    public AnalyzeMatchComparisonCommand(
 24        IAnsiConsole console,
 25        IFirebaseServiceFactory firebaseServiceFactory,
 26        IKicktippClientFactory kicktippClientFactory,
 27        IOpenAiServiceFactory openAiServiceFactory)
 28    {
 29        _console = console;
 30        _firebaseServiceFactory = firebaseServiceFactory;
 31        _kicktippClientFactory = kicktippClientFactory;
 32        _openAiServiceFactory = openAiServiceFactory;
 33    }
 34
 35    public override async Task<int> ExecuteAsync(CommandContext context, AnalyzeMatchComparisonSettings settings)
 36    {
 37        var loggerFactory = AnalyzeMatchCommandHelpers.CreateLoggerFactory(settings.Debug);
 38        var logger = loggerFactory.CreateLogger<AnalyzeMatchComparisonCommand>();
 39
 40        try
 41        {
 42            var validation = settings.Validate();
 43            if (!validation.Successful)
 44            {
 45                _console.MarkupLine($"[red]Error:[/] {validation.Message}");
 46                return 1;
 47            }
 48
 49            var predictionService = _openAiServiceFactory.CreatePredictionService(settings.Model);
 50            var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 51            var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 52            var kicktippClient = _kicktippClientFactory.CreateClient();
 53
 54            var communityContext = settings.CommunityContext!;
 55
 56            _console.MarkupLine($"[green]Analyze match comparison initialized with model:[/] [yellow]{settings.Model}[/]
 57            _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 58            _console.MarkupLine($"[blue]Runs per mode:[/] [yellow]{settings.Runs}[/]");
 59
 60            if (settings.Debug)
 61            {
 62                _console.MarkupLine("[dim]Debug logging enabled[/]");
 63            }
 64
 65            var match = await AnalyzeMatchCommandHelpers.ResolveMatchAsync(settings, kicktippClient, logger, communityCo
 66            if (match == null)
 67            {
 68                _console.MarkupLine("[red]Failed to resolve match details. Aborting.[/]");
 69                return 1;
 70            }
 71
 72            var contextDocuments = new List<DocumentContext>();
 73
 74            if (contextRepository != null)
 75            {
 76                var contextDocumentInfos = await AnalyzeMatchCommandHelpers.GetMatchContextDocumentsAsync(
 77                    contextRepository,
 78                    match.HomeTeam,
 79                    match.AwayTeam,
 80                    communityContext,
 81                    settings.Verbose,
 82                    _console);
 83
 84                contextDocuments = contextDocumentInfos.Select(info => info.Document).ToList();
 85
 86                if (contextDocumentInfos.Any())
 87                {
 88                    _console.MarkupLine("[dim]Loaded context documents:[/]");
 89                    foreach (var info in contextDocumentInfos)
 90                    {
 91                        _console.MarkupLine($"[grey]  • {info.Document.Name}[/] [dim](v{info.Version})[/]");
 92
 93                        if (settings.ShowContextDocuments)
 94                        {
 95                            var lines = info.Document.Content.Split('\n');
 96                            foreach (var line in lines.Take(10))
 97                            {
 98                                _console.MarkupLine($"[grey]      {line.EscapeMarkup()}[/]");
 99                            }
 100
 101                            if (lines.Length > 10)
 102                            {
 103                                _console.MarkupLine($"[dim]      ... ({lines.Length - 10} more lines) ...[/]");
 104                            }
 105                        }
 106                    }
 107                }
 108                else
 109                {
 110                    _console.MarkupLine("[yellow]No context documents retrieved; proceeding without additional context[/
 111                }
 112            }
 113            else
 114            {
 115                _console.MarkupLine("[yellow]Context repository not configured. Proceeding without context documents.[/]
 116            }
 117
 118            tokenUsageTracker.Reset();
 119
 120            string FormatCurrencyValue(decimal value) => $"${value.ToString("F4", CultureInfo.InvariantCulture)}";
 121            string FormatCurrencyOptional(decimal? value) => value.HasValue ? FormatCurrencyValue(value.Value) : "n/a";
 122            string FormatDurationValue(TimeSpan value) => value.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCultur
 123            string FormatDurationOptional(TimeSpan? value) => value.HasValue ? FormatDurationValue(value.Value) : "n/a";
 124            string ModeLabel(bool includeJustification) => includeJustification ? "with justification" : "without justif
 125
 126            var runResults = new List<ComparisonRunResult>();
 127
 128            for (var run = 1; run <= settings.Runs; run++)
 129            {
 130                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: true));
 131                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: false));
 132            }
 133
 134            PrintSummary(runResults);
 135
 136            return 0;
 137
 138            async Task<ComparisonRunResult> ExecuteSingleRunAsync(int runNumber, bool includeJustification)
 139            {
 140                var label = ModeLabel(includeJustification);
 141                _console.MarkupLine($"[cyan]\nRun {runNumber}/{settings.Runs} — {label}[/]");
 142
 143                var stopwatch = Stopwatch.StartNew();
 144                var prediction = await predictionService.PredictMatchAsync(
 145                    match,
 146                    contextDocuments,
 147                    includeJustification);
 148                stopwatch.Stop();
 149
 150                if (prediction == null)
 151                {
 152                    _console.MarkupLine("[red]  ✗ Prediction failed[/]");
 153                    return new ComparisonRunResult(runNumber, includeJustification, null, stopwatch.Elapsed, null, false
 154                }
 155
 156                var lastCost = tokenUsageTracker.GetLastCost();
 157                var usageSummary = tokenUsageTracker.GetLastUsageCompactSummary();
 158
 159                _console.MarkupLine($"[green]  ✓ Prediction:[/] [yellow]{prediction.HomeGoals}:{prediction.AwayGoals}[/]
 160                _console.MarkupLine($"[magenta]  ↳ Cost:[/] [cyan]{FormatCurrencyValue(lastCost)}[/] [grey]({usageSummar
 161
 162                return new ComparisonRunResult(runNumber, includeJustification, prediction, stopwatch.Elapsed, lastCost,
 163            }
 164
 165            void PrintSummary(List<ComparisonRunResult> results)
 166            {
 167                _console.MarkupLine("\n[bold yellow]Comparison Summary[/]");
 168
 169                var summaryTable = new Table()
 170                    .Border(TableBorder.Rounded)
 171                    .AddColumn(new TableColumn("[grey]Mode[/]").LeftAligned())
 172                    .AddColumn(new TableColumn("[grey]Successful[/]").RightAligned())
 173                    .AddColumn(new TableColumn("[grey]Failed[/]").RightAligned())
 174                    .AddColumn(new TableColumn("[grey]Total cost[/]").RightAligned())
 175                    .AddColumn(new TableColumn("[grey]Average cost[/]").RightAligned())
 176                    .AddColumn(new TableColumn("[grey]Average duration[/]").RightAligned());
 177
 178                foreach (var includeJustification in new[] { true, false })
 179                {
 180                    var modeResults = results.Where(r => r.IncludeJustification == includeJustification).ToList();
 181                    var successful = modeResults.Where(r => r.Success).ToList();
 182                    var failures = modeResults.Count - successful.Count;
 183                    var totalCost = successful.Where(r => r.Cost.HasValue).Sum(r => r.Cost!.Value);
 184                    var averageCost = successful.Count > 0 ? totalCost / successful.Count : (decimal?)null;
 185                    var totalDuration = modeResults.Aggregate(TimeSpan.Zero, (current, result) => current + result.Durat
 186                    TimeSpan? averageDuration = modeResults.Count > 0
 187                        ? TimeSpan.FromTicks(totalDuration.Ticks / modeResults.Count)
 188                        : (TimeSpan?)null;
 189
 190                    summaryTable.AddRow(
 191                        includeJustification ? "With justification" : "Without justification",
 192                        successful.Count.ToString(CultureInfo.InvariantCulture),
 193                        failures.ToString(CultureInfo.InvariantCulture),
 194                        FormatCurrencyValue(totalCost),
 195                        FormatCurrencyOptional(averageCost),
 196                        FormatDurationOptional(averageDuration));
 197                }
 198
 199                summaryTable.AddRow(
 200                    "Combined",
 201                    results.Count(r => r.Success).ToString(CultureInfo.InvariantCulture),
 202                    results.Count(r => !r.Success).ToString(CultureInfo.InvariantCulture),
 203                    FormatCurrencyValue(tokenUsageTracker.GetTotalCost()),
 204                    "n/a",
 205                    "n/a");
 206
 207                _console.Write(summaryTable);
 208
 209                var distributions = results
 210                    .Where(r => r.Success && r.Prediction != null)
 211                    .GroupBy(r => new
 212                    {
 213                        r.IncludeJustification,
 214                        Score = $"{r.Prediction!.HomeGoals}:{r.Prediction!.AwayGoals}"
 215                    })
 216                    .ToDictionary(group => group.Key, group => group.Count());
 217
 218                if (!distributions.Any())
 219                {
 220                    _console.MarkupLine("[yellow]No successful predictions to compare.[/]");
 221                    return;
 222                }
 223
 224                var scores = distributions.Keys
 225                    .Select(key => key.Score)
 226                    .Distinct()
 227                    .OrderBy(score => score, StringComparer.Ordinal)
 228                    .ToList();
 229
 230                var distributionTable = new Table()
 231                    .Title("[bold blue]Prediction distribution[/]")
 232                    .Border(TableBorder.Rounded)
 233                    .AddColumn(new TableColumn("[grey]Score[/]").LeftAligned())
 234                    .AddColumn(new TableColumn("[grey]With justification[/]").RightAligned())
 235                    .AddColumn(new TableColumn("[grey]Without justification[/]").RightAligned());
 236
 237                foreach (var score in scores)
 238                {
 239                    distributions.TryGetValue(new { IncludeJustification = true, Score = score }, out var withCount);
 240                    distributions.TryGetValue(new { IncludeJustification = false, Score = score }, out var withoutCount)
 241
 242                    distributionTable.AddRow(
 243                        score,
 244                        withCount.ToString(CultureInfo.InvariantCulture),
 245                        withoutCount.ToString(CultureInfo.InvariantCulture));
 246                }
 247
 248                _console.Write(distributionTable);
 249            }
 250        }
 251        catch (Exception ex)
 252        {
 253            logger.LogError(ex, "Error executing analyze-match comparison command");
 254            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 255            return 1;
 256        }
 257        finally
 258        {
 259            loggerFactory.Dispose();
 260        }
 261    }
 262
 1263    private sealed record ComparisonRunResult(int RunNumber, bool IncludeJustification, Prediction? Prediction, TimeSpan
 264}