< Summary

Information
Class: Orchestrator.Commands.Observability.AnalyzeMatch.AnalyzeMatchComparisonCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/AnalyzeMatch/AnalyzeMatchComparisonCommand.cs
Line coverage
96%
Covered lines: 148
Uncovered lines: 5
Coverable lines: 153
Total lines: 264
Line coverage: 96.7%
Branch coverage
94%
Covered branches: 64
Total branches: 68
Branch coverage: 94.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()90.91%222292.73%
FormatCurrencyValue()100%11100%
FormatCurrencyOptional()100%22100%
FormatDurationValue()100%11100%
FormatDurationOptional()50%22100%
ModeLabel()100%22100%
ExecuteSingleRunAsync()100%22100%
PrintSummary()97.22%3636100%
.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
 123    public AnalyzeMatchComparisonCommand(
 124        IAnsiConsole console,
 125        IFirebaseServiceFactory firebaseServiceFactory,
 126        IKicktippClientFactory kicktippClientFactory,
 127        IOpenAiServiceFactory openAiServiceFactory)
 28    {
 129        _console = console;
 130        _firebaseServiceFactory = firebaseServiceFactory;
 131        _kicktippClientFactory = kicktippClientFactory;
 132        _openAiServiceFactory = openAiServiceFactory;
 133    }
 34
 35    public override async Task<int> ExecuteAsync(CommandContext context, AnalyzeMatchComparisonSettings settings)
 36    {
 137        var loggerFactory = AnalyzeMatchCommandHelpers.CreateLoggerFactory(settings.Debug);
 138        var logger = loggerFactory.CreateLogger<AnalyzeMatchComparisonCommand>();
 39
 40        try
 41        {
 142            var validation = settings.Validate();
 143            if (!validation.Successful)
 44            {
 045                _console.MarkupLine($"[red]Error:[/] {validation.Message}");
 046                return 1;
 47            }
 48
 149            var predictionService = _openAiServiceFactory.CreatePredictionService(settings.Model);
 150            var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 151            var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 152            var kicktippClient = _kicktippClientFactory.CreateClient();
 53
 154            var communityContext = settings.CommunityContext!;
 55
 156            _console.MarkupLine($"[green]Analyze match comparison initialized with model:[/] [yellow]{settings.Model}[/]
 157            _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 158            _console.MarkupLine($"[blue]Runs per mode:[/] [yellow]{settings.Runs}[/]");
 59
 160            if (settings.Debug)
 61            {
 162                _console.MarkupLine("[dim]Debug logging enabled[/]");
 63            }
 64
 165            var match = await AnalyzeMatchCommandHelpers.ResolveMatchAsync(settings, kicktippClient, logger, communityCo
 166            if (match == null)
 67            {
 068                _console.MarkupLine("[red]Failed to resolve match details. Aborting.[/]");
 069                return 1;
 70            }
 71
 172            var contextDocuments = new List<DocumentContext>();
 73
 174            if (contextRepository != null)
 75            {
 176                var contextDocumentInfos = await AnalyzeMatchCommandHelpers.GetMatchContextDocumentsAsync(
 177                    contextRepository,
 178                    match.HomeTeam,
 179                    match.AwayTeam,
 180                    communityContext,
 181                    settings.Verbose,
 182                    _console);
 83
 184                contextDocuments = contextDocumentInfos.Select(info => info.Document).ToList();
 85
 186                if (contextDocumentInfos.Any())
 87                {
 188                    _console.MarkupLine("[dim]Loaded context documents:[/]");
 189                    foreach (var info in contextDocumentInfos)
 90                    {
 191                        _console.MarkupLine($"[grey]  • {info.Document.Name}[/] [dim](v{info.Version})[/]");
 92
 193                        if (settings.ShowContextDocuments)
 94                        {
 195                            var lines = info.Document.Content.Split('\n');
 196                            foreach (var line in lines.Take(10))
 97                            {
 198                                _console.MarkupLine($"[grey]      {line.EscapeMarkup()}[/]");
 99                            }
 100
 1101                            if (lines.Length > 10)
 102                            {
 1103                                _console.MarkupLine($"[dim]      ... ({lines.Length - 10} more lines) ...[/]");
 104                            }
 105                        }
 106                    }
 107                }
 108                else
 109                {
 1110                    _console.MarkupLine("[yellow]No context documents retrieved; proceeding without additional context[/
 111                }
 112            }
 113            else
 114            {
 1115                _console.MarkupLine("[yellow]Context repository not configured. Proceeding without context documents.[/]
 116            }
 117
 1118            tokenUsageTracker.Reset();
 119
 1120            string FormatCurrencyValue(decimal value) => $"${value.ToString("F4", CultureInfo.InvariantCulture)}";
 1121            string FormatCurrencyOptional(decimal? value) => value.HasValue ? FormatCurrencyValue(value.Value) : "n/a";
 1122            string FormatDurationValue(TimeSpan value) => value.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCultur
 1123            string FormatDurationOptional(TimeSpan? value) => value.HasValue ? FormatDurationValue(value.Value) : "n/a";
 1124            string ModeLabel(bool includeJustification) => includeJustification ? "with justification" : "without justif
 125
 1126            var runResults = new List<ComparisonRunResult>();
 127
 1128            for (var run = 1; run <= settings.Runs; run++)
 129            {
 1130                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: true));
 1131                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: false));
 132            }
 133
 1134            PrintSummary(runResults);
 135
 1136            return 0;
 137
 138            async Task<ComparisonRunResult> ExecuteSingleRunAsync(int runNumber, bool includeJustification)
 139            {
 1140                var label = ModeLabel(includeJustification);
 1141                _console.MarkupLine($"[cyan]\nRun {runNumber}/{settings.Runs} — {label}[/]");
 142
 1143                var stopwatch = Stopwatch.StartNew();
 1144                var prediction = await predictionService.PredictMatchAsync(
 1145                    match,
 1146                    contextDocuments,
 1147                    includeJustification);
 1148                stopwatch.Stop();
 149
 1150                if (prediction == null)
 151                {
 1152                    _console.MarkupLine("[red]  ✗ Prediction failed[/]");
 1153                    return new ComparisonRunResult(runNumber, includeJustification, null, stopwatch.Elapsed, null, false
 154                }
 155
 1156                var lastCost = tokenUsageTracker.GetLastCost();
 1157                var usageSummary = tokenUsageTracker.GetLastUsageCompactSummary();
 158
 1159                _console.MarkupLine($"[green]  ✓ Prediction:[/] [yellow]{prediction.HomeGoals}:{prediction.AwayGoals}[/]
 1160                _console.MarkupLine($"[magenta]  ↳ Cost:[/] [cyan]{FormatCurrencyValue(lastCost)}[/] [grey]({usageSummar
 161
 1162                return new ComparisonRunResult(runNumber, includeJustification, prediction, stopwatch.Elapsed, lastCost,
 1163            }
 164
 165            void PrintSummary(List<ComparisonRunResult> results)
 166            {
 1167                _console.MarkupLine("\n[bold yellow]Comparison Summary[/]");
 168
 1169                var summaryTable = new Table()
 1170                    .Border(TableBorder.Rounded)
 1171                    .AddColumn(new TableColumn("[grey]Mode[/]").LeftAligned())
 1172                    .AddColumn(new TableColumn("[grey]Successful[/]").RightAligned())
 1173                    .AddColumn(new TableColumn("[grey]Failed[/]").RightAligned())
 1174                    .AddColumn(new TableColumn("[grey]Total cost[/]").RightAligned())
 1175                    .AddColumn(new TableColumn("[grey]Average cost[/]").RightAligned())
 1176                    .AddColumn(new TableColumn("[grey]Average duration[/]").RightAligned());
 177
 1178                foreach (var includeJustification in new[] { true, false })
 179                {
 1180                    var modeResults = results.Where(r => r.IncludeJustification == includeJustification).ToList();
 1181                    var successful = modeResults.Where(r => r.Success).ToList();
 1182                    var failures = modeResults.Count - successful.Count;
 1183                    var totalCost = successful.Where(r => r.Cost.HasValue).Sum(r => r.Cost!.Value);
 1184                    var averageCost = successful.Count > 0 ? totalCost / successful.Count : (decimal?)null;
 1185                    var totalDuration = modeResults.Aggregate(TimeSpan.Zero, (current, result) => current + result.Durat
 1186                    TimeSpan? averageDuration = modeResults.Count > 0
 1187                        ? TimeSpan.FromTicks(totalDuration.Ticks / modeResults.Count)
 1188                        : (TimeSpan?)null;
 189
 1190                    summaryTable.AddRow(
 1191                        includeJustification ? "With justification" : "Without justification",
 1192                        successful.Count.ToString(CultureInfo.InvariantCulture),
 1193                        failures.ToString(CultureInfo.InvariantCulture),
 1194                        FormatCurrencyValue(totalCost),
 1195                        FormatCurrencyOptional(averageCost),
 1196                        FormatDurationOptional(averageDuration));
 197                }
 198
 1199                summaryTable.AddRow(
 1200                    "Combined",
 1201                    results.Count(r => r.Success).ToString(CultureInfo.InvariantCulture),
 1202                    results.Count(r => !r.Success).ToString(CultureInfo.InvariantCulture),
 1203                    FormatCurrencyValue(tokenUsageTracker.GetTotalCost()),
 1204                    "n/a",
 1205                    "n/a");
 206
 1207                _console.Write(summaryTable);
 208
 1209                var distributions = results
 1210                    .Where(r => r.Success && r.Prediction != null)
 1211                    .GroupBy(r => new
 1212                    {
 1213                        r.IncludeJustification,
 1214                        Score = $"{r.Prediction!.HomeGoals}:{r.Prediction!.AwayGoals}"
 1215                    })
 1216                    .ToDictionary(group => group.Key, group => group.Count());
 217
 1218                if (!distributions.Any())
 219                {
 1220                    _console.MarkupLine("[yellow]No successful predictions to compare.[/]");
 1221                    return;
 222                }
 223
 1224                var scores = distributions.Keys
 1225                    .Select(key => key.Score)
 1226                    .Distinct()
 0227                    .OrderBy(score => score, StringComparer.Ordinal)
 1228                    .ToList();
 229
 1230                var distributionTable = new Table()
 1231                    .Title("[bold blue]Prediction distribution[/]")
 1232                    .Border(TableBorder.Rounded)
 1233                    .AddColumn(new TableColumn("[grey]Score[/]").LeftAligned())
 1234                    .AddColumn(new TableColumn("[grey]With justification[/]").RightAligned())
 1235                    .AddColumn(new TableColumn("[grey]Without justification[/]").RightAligned());
 236
 1237                foreach (var score in scores)
 238                {
 1239                    distributions.TryGetValue(new { IncludeJustification = true, Score = score }, out var withCount);
 1240                    distributions.TryGetValue(new { IncludeJustification = false, Score = score }, out var withoutCount)
 241
 1242                    distributionTable.AddRow(
 1243                        score,
 1244                        withCount.ToString(CultureInfo.InvariantCulture),
 1245                        withoutCount.ToString(CultureInfo.InvariantCulture));
 246                }
 247
 1248                _console.Write(distributionTable);
 1249            }
 250        }
 1251        catch (Exception ex)
 252        {
 1253            logger.LogError(ex, "Error executing analyze-match comparison command");
 1254            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 1255            return 1;
 256        }
 257        finally
 258        {
 1259            loggerFactory.Dispose();
 260        }
 1261    }
 262
 1263    private sealed record ComparisonRunResult(int RunNumber, bool IncludeJustification, Prediction? Prediction, TimeSpan
 264}