< 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
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 263
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_IncludeJustification()100%210%
set_IncludeJustification(...)100%210%
get_Prediction()100%210%
set_Prediction(...)100%210%
get_Duration()100%210%
set_Duration(...)100%210%
get_Cost()100%210%
set_Cost(...)100%210%
get_Success()100%210%
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
 83                contextDocuments = contextDocumentInfos.Select(info => info.Document).ToList();
 84
 85                if (contextDocumentInfos.Any())
 86                {
 87                    _console.MarkupLine("[dim]Loaded context documents:[/]");
 88                    foreach (var info in contextDocumentInfos)
 89                    {
 90                        _console.MarkupLine($"[grey]  • {info.Document.Name}[/] [dim](v{info.Version})[/]");
 91
 92                        if (settings.ShowContextDocuments)
 93                        {
 94                            var lines = info.Document.Content.Split('\n');
 95                            foreach (var line in lines.Take(10))
 96                            {
 97                                _console.MarkupLine($"[grey]      {line.EscapeMarkup()}[/]");
 98                            }
 99
 100                            if (lines.Length > 10)
 101                            {
 102                                _console.MarkupLine($"[dim]      ... ({lines.Length - 10} more lines) ...[/]");
 103                            }
 104                        }
 105                    }
 106                }
 107                else
 108                {
 109                    _console.MarkupLine("[yellow]No context documents retrieved; proceeding without additional context[/
 110                }
 111            }
 112            else
 113            {
 114                _console.MarkupLine("[yellow]Context repository not configured. Proceeding without context documents.[/]
 115            }
 116
 117            tokenUsageTracker.Reset();
 118
 119            string FormatCurrencyValue(decimal value) => $"${value.ToString("F4", CultureInfo.InvariantCulture)}";
 120            string FormatCurrencyOptional(decimal? value) => value.HasValue ? FormatCurrencyValue(value.Value) : "n/a";
 121            string FormatDurationValue(TimeSpan value) => value.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCultur
 122            string FormatDurationOptional(TimeSpan? value) => value.HasValue ? FormatDurationValue(value.Value) : "n/a";
 123            string ModeLabel(bool includeJustification) => includeJustification ? "with justification" : "without justif
 124
 125            var runResults = new List<ComparisonRunResult>();
 126
 127            for (var run = 1; run <= settings.Runs; run++)
 128            {
 129                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: true));
 130                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: false));
 131            }
 132
 133            PrintSummary(runResults);
 134
 135            return 0;
 136
 137            async Task<ComparisonRunResult> ExecuteSingleRunAsync(int runNumber, bool includeJustification)
 138            {
 139                var label = ModeLabel(includeJustification);
 140                _console.MarkupLine($"[cyan]\nRun {runNumber}/{settings.Runs} — {label}[/]");
 141
 142                var stopwatch = Stopwatch.StartNew();
 143                var prediction = await predictionService.PredictMatchAsync(
 144                    match,
 145                    contextDocuments,
 146                    includeJustification);
 147                stopwatch.Stop();
 148
 149                if (prediction == null)
 150                {
 151                    _console.MarkupLine("[red]  ✗ Prediction failed[/]");
 152                    return new ComparisonRunResult(runNumber, includeJustification, null, stopwatch.Elapsed, null, false
 153                }
 154
 155                var lastCost = tokenUsageTracker.GetLastCost();
 156                var usageSummary = tokenUsageTracker.GetLastUsageCompactSummary();
 157
 158                _console.MarkupLine($"[green]  ✓ Prediction:[/] [yellow]{prediction.HomeGoals}:{prediction.AwayGoals}[/]
 159                _console.MarkupLine($"[magenta]  ↳ Cost:[/] [cyan]{FormatCurrencyValue(lastCost)}[/] [grey]({usageSummar
 160
 161                return new ComparisonRunResult(runNumber, includeJustification, prediction, stopwatch.Elapsed, lastCost,
 162            }
 163
 164            void PrintSummary(List<ComparisonRunResult> results)
 165            {
 166                _console.MarkupLine("\n[bold yellow]Comparison Summary[/]");
 167
 168                var summaryTable = new Table()
 169                    .Border(TableBorder.Rounded)
 170                    .AddColumn(new TableColumn("[grey]Mode[/]").LeftAligned())
 171                    .AddColumn(new TableColumn("[grey]Successful[/]").RightAligned())
 172                    .AddColumn(new TableColumn("[grey]Failed[/]").RightAligned())
 173                    .AddColumn(new TableColumn("[grey]Total cost[/]").RightAligned())
 174                    .AddColumn(new TableColumn("[grey]Average cost[/]").RightAligned())
 175                    .AddColumn(new TableColumn("[grey]Average duration[/]").RightAligned());
 176
 177                foreach (var includeJustification in new[] { true, false })
 178                {
 179                    var modeResults = results.Where(r => r.IncludeJustification == includeJustification).ToList();
 180                    var successful = modeResults.Where(r => r.Success).ToList();
 181                    var failures = modeResults.Count - successful.Count;
 182                    var totalCost = successful.Where(r => r.Cost.HasValue).Sum(r => r.Cost!.Value);
 183                    var averageCost = successful.Count > 0 ? totalCost / successful.Count : (decimal?)null;
 184                    var totalDuration = modeResults.Aggregate(TimeSpan.Zero, (current, result) => current + result.Durat
 185                    TimeSpan? averageDuration = modeResults.Count > 0
 186                        ? TimeSpan.FromTicks(totalDuration.Ticks / modeResults.Count)
 187                        : (TimeSpan?)null;
 188
 189                    summaryTable.AddRow(
 190                        includeJustification ? "With justification" : "Without justification",
 191                        successful.Count.ToString(CultureInfo.InvariantCulture),
 192                        failures.ToString(CultureInfo.InvariantCulture),
 193                        FormatCurrencyValue(totalCost),
 194                        FormatCurrencyOptional(averageCost),
 195                        FormatDurationOptional(averageDuration));
 196                }
 197
 198                summaryTable.AddRow(
 199                    "Combined",
 200                    results.Count(r => r.Success).ToString(CultureInfo.InvariantCulture),
 201                    results.Count(r => !r.Success).ToString(CultureInfo.InvariantCulture),
 202                    FormatCurrencyValue(tokenUsageTracker.GetTotalCost()),
 203                    "n/a",
 204                    "n/a");
 205
 206                _console.Write(summaryTable);
 207
 208                var distributions = results
 209                    .Where(r => r.Success && r.Prediction != null)
 210                    .GroupBy(r => new
 211                    {
 212                        r.IncludeJustification,
 213                        Score = $"{r.Prediction!.HomeGoals}:{r.Prediction!.AwayGoals}"
 214                    })
 215                    .ToDictionary(group => group.Key, group => group.Count());
 216
 217                if (!distributions.Any())
 218                {
 219                    _console.MarkupLine("[yellow]No successful predictions to compare.[/]");
 220                    return;
 221                }
 222
 223                var scores = distributions.Keys
 224                    .Select(key => key.Score)
 225                    .Distinct()
 226                    .OrderBy(score => score, StringComparer.Ordinal)
 227                    .ToList();
 228
 229                var distributionTable = new Table()
 230                    .Title("[bold blue]Prediction distribution[/]")
 231                    .Border(TableBorder.Rounded)
 232                    .AddColumn(new TableColumn("[grey]Score[/]").LeftAligned())
 233                    .AddColumn(new TableColumn("[grey]With justification[/]").RightAligned())
 234                    .AddColumn(new TableColumn("[grey]Without justification[/]").RightAligned());
 235
 236                foreach (var score in scores)
 237                {
 238                    distributions.TryGetValue(new { IncludeJustification = true, Score = score }, out var withCount);
 239                    distributions.TryGetValue(new { IncludeJustification = false, Score = score }, out var withoutCount)
 240
 241                    distributionTable.AddRow(
 242                        score,
 243                        withCount.ToString(CultureInfo.InvariantCulture),
 244                        withoutCount.ToString(CultureInfo.InvariantCulture));
 245                }
 246
 247                _console.Write(distributionTable);
 248            }
 249        }
 250        catch (Exception ex)
 251        {
 252            logger.LogError(ex, "Error executing analyze-match comparison command");
 253            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 254            return 1;
 255        }
 256        finally
 257        {
 258            loggerFactory.Dispose();
 259        }
 260    }
 261
 0262    private sealed record ComparisonRunResult(int RunNumber, bool IncludeJustification, Prediction? Prediction, TimeSpan
 263}