< 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
0%
Covered lines: 0
Uncovered lines: 152
Coverable lines: 152
Total lines: 263
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 68
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
ExecuteAsync()0%506220%
FormatCurrencyValue()100%210%
FormatCurrencyOptional()0%620%
FormatDurationValue()100%210%
FormatDurationOptional()0%620%
ModeLabel()0%620%
ExecuteSingleRunAsync()0%620%
PrintSummary()0%1332360%
.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
 023    public AnalyzeMatchComparisonCommand(
 024        IAnsiConsole console,
 025        IFirebaseServiceFactory firebaseServiceFactory,
 026        IKicktippClientFactory kicktippClientFactory,
 027        IOpenAiServiceFactory openAiServiceFactory)
 28    {
 029        _console = console;
 030        _firebaseServiceFactory = firebaseServiceFactory;
 031        _kicktippClientFactory = kicktippClientFactory;
 032        _openAiServiceFactory = openAiServiceFactory;
 033    }
 34
 35    public override async Task<int> ExecuteAsync(CommandContext context, AnalyzeMatchComparisonSettings settings)
 36    {
 037        var loggerFactory = AnalyzeMatchCommandHelpers.CreateLoggerFactory(settings.Debug);
 038        var logger = loggerFactory.CreateLogger<AnalyzeMatchComparisonCommand>();
 39
 40        try
 41        {
 042            var validation = settings.Validate();
 043            if (!validation.Successful)
 44            {
 045                _console.MarkupLine($"[red]Error:[/] {validation.Message}");
 046                return 1;
 47            }
 48
 049            var predictionService = _openAiServiceFactory.CreatePredictionService(settings.Model);
 050            var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 051            var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 052            var kicktippClient = _kicktippClientFactory.CreateClient();
 53
 054            var communityContext = settings.CommunityContext!;
 55
 056            _console.MarkupLine($"[green]Analyze match comparison initialized with model:[/] [yellow]{settings.Model}[/]
 057            _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 058            _console.MarkupLine($"[blue]Runs per mode:[/] [yellow]{settings.Runs}[/]");
 59
 060            if (settings.Debug)
 61            {
 062                _console.MarkupLine("[dim]Debug logging enabled[/]");
 63            }
 64
 065            var match = await AnalyzeMatchCommandHelpers.ResolveMatchAsync(settings, kicktippClient, logger, communityCo
 066            if (match == null)
 67            {
 068                _console.MarkupLine("[red]Failed to resolve match details. Aborting.[/]");
 069                return 1;
 70            }
 71
 072            var contextDocuments = new List<DocumentContext>();
 73
 074            if (contextRepository != null)
 75            {
 076                var contextDocumentInfos = await AnalyzeMatchCommandHelpers.GetMatchContextDocumentsAsync(
 077                    contextRepository,
 078                    match.HomeTeam,
 079                    match.AwayTeam,
 080                    communityContext,
 081                    settings.Verbose);
 82
 083                contextDocuments = contextDocumentInfos.Select(info => info.Document).ToList();
 84
 085                if (contextDocumentInfos.Any())
 86                {
 087                    _console.MarkupLine("[dim]Loaded context documents:[/]");
 088                    foreach (var info in contextDocumentInfos)
 89                    {
 090                        _console.MarkupLine($"[grey]  • {info.Document.Name}[/] [dim](v{info.Version})[/]");
 91
 092                        if (settings.ShowContextDocuments)
 93                        {
 094                            var lines = info.Document.Content.Split('\n');
 095                            foreach (var line in lines.Take(10))
 96                            {
 097                                _console.MarkupLine($"[grey]      {line.EscapeMarkup()}[/]");
 98                            }
 99
 0100                            if (lines.Length > 10)
 101                            {
 0102                                _console.MarkupLine($"[dim]      ... ({lines.Length - 10} more lines) ...[/]");
 103                            }
 104                        }
 105                    }
 106                }
 107                else
 108                {
 0109                    _console.MarkupLine("[yellow]No context documents retrieved; proceeding without additional context[/
 110                }
 111            }
 112            else
 113            {
 0114                _console.MarkupLine("[yellow]Context repository not configured. Proceeding without context documents.[/]
 115            }
 116
 0117            tokenUsageTracker.Reset();
 118
 0119            string FormatCurrencyValue(decimal value) => $"${value.ToString("F4", CultureInfo.InvariantCulture)}";
 0120            string FormatCurrencyOptional(decimal? value) => value.HasValue ? FormatCurrencyValue(value.Value) : "n/a";
 0121            string FormatDurationValue(TimeSpan value) => value.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCultur
 0122            string FormatDurationOptional(TimeSpan? value) => value.HasValue ? FormatDurationValue(value.Value) : "n/a";
 0123            string ModeLabel(bool includeJustification) => includeJustification ? "with justification" : "without justif
 124
 0125            var runResults = new List<ComparisonRunResult>();
 126
 0127            for (var run = 1; run <= settings.Runs; run++)
 128            {
 0129                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: true));
 0130                runResults.Add(await ExecuteSingleRunAsync(run, includeJustification: false));
 131            }
 132
 0133            PrintSummary(runResults);
 134
 0135            return 0;
 136
 137            async Task<ComparisonRunResult> ExecuteSingleRunAsync(int runNumber, bool includeJustification)
 138            {
 0139                var label = ModeLabel(includeJustification);
 0140                _console.MarkupLine($"[cyan]\nRun {runNumber}/{settings.Runs} — {label}[/]");
 141
 0142                var stopwatch = Stopwatch.StartNew();
 0143                var prediction = await predictionService.PredictMatchAsync(
 0144                    match,
 0145                    contextDocuments,
 0146                    includeJustification);
 0147                stopwatch.Stop();
 148
 0149                if (prediction == null)
 150                {
 0151                    _console.MarkupLine("[red]  ✗ Prediction failed[/]");
 0152                    return new ComparisonRunResult(runNumber, includeJustification, null, stopwatch.Elapsed, null, false
 153                }
 154
 0155                var lastCost = tokenUsageTracker.GetLastCost();
 0156                var usageSummary = tokenUsageTracker.GetLastUsageCompactSummary();
 157
 0158                _console.MarkupLine($"[green]  ✓ Prediction:[/] [yellow]{prediction.HomeGoals}:{prediction.AwayGoals}[/]
 0159                _console.MarkupLine($"[magenta]  ↳ Cost:[/] [cyan]{FormatCurrencyValue(lastCost)}[/] [grey]({usageSummar
 160
 0161                return new ComparisonRunResult(runNumber, includeJustification, prediction, stopwatch.Elapsed, lastCost,
 0162            }
 163
 164            void PrintSummary(List<ComparisonRunResult> results)
 165            {
 0166                _console.MarkupLine("\n[bold yellow]Comparison Summary[/]");
 167
 0168                var summaryTable = new Table()
 0169                    .Border(TableBorder.Rounded)
 0170                    .AddColumn(new TableColumn("[grey]Mode[/]").LeftAligned())
 0171                    .AddColumn(new TableColumn("[grey]Successful[/]").RightAligned())
 0172                    .AddColumn(new TableColumn("[grey]Failed[/]").RightAligned())
 0173                    .AddColumn(new TableColumn("[grey]Total cost[/]").RightAligned())
 0174                    .AddColumn(new TableColumn("[grey]Average cost[/]").RightAligned())
 0175                    .AddColumn(new TableColumn("[grey]Average duration[/]").RightAligned());
 176
 0177                foreach (var includeJustification in new[] { true, false })
 178                {
 0179                    var modeResults = results.Where(r => r.IncludeJustification == includeJustification).ToList();
 0180                    var successful = modeResults.Where(r => r.Success).ToList();
 0181                    var failures = modeResults.Count - successful.Count;
 0182                    var totalCost = successful.Where(r => r.Cost.HasValue).Sum(r => r.Cost!.Value);
 0183                    var averageCost = successful.Count > 0 ? totalCost / successful.Count : (decimal?)null;
 0184                    var totalDuration = modeResults.Aggregate(TimeSpan.Zero, (current, result) => current + result.Durat
 0185                    TimeSpan? averageDuration = modeResults.Count > 0
 0186                        ? TimeSpan.FromTicks(totalDuration.Ticks / modeResults.Count)
 0187                        : (TimeSpan?)null;
 188
 0189                    summaryTable.AddRow(
 0190                        includeJustification ? "With justification" : "Without justification",
 0191                        successful.Count.ToString(CultureInfo.InvariantCulture),
 0192                        failures.ToString(CultureInfo.InvariantCulture),
 0193                        FormatCurrencyValue(totalCost),
 0194                        FormatCurrencyOptional(averageCost),
 0195                        FormatDurationOptional(averageDuration));
 196                }
 197
 0198                summaryTable.AddRow(
 0199                    "Combined",
 0200                    results.Count(r => r.Success).ToString(CultureInfo.InvariantCulture),
 0201                    results.Count(r => !r.Success).ToString(CultureInfo.InvariantCulture),
 0202                    FormatCurrencyValue(tokenUsageTracker.GetTotalCost()),
 0203                    "n/a",
 0204                    "n/a");
 205
 0206                _console.Write(summaryTable);
 207
 0208                var distributions = results
 0209                    .Where(r => r.Success && r.Prediction != null)
 0210                    .GroupBy(r => new
 0211                    {
 0212                        r.IncludeJustification,
 0213                        Score = $"{r.Prediction!.HomeGoals}:{r.Prediction!.AwayGoals}"
 0214                    })
 0215                    .ToDictionary(group => group.Key, group => group.Count());
 216
 0217                if (!distributions.Any())
 218                {
 0219                    _console.MarkupLine("[yellow]No successful predictions to compare.[/]");
 0220                    return;
 221                }
 222
 0223                var scores = distributions.Keys
 0224                    .Select(key => key.Score)
 0225                    .Distinct()
 0226                    .OrderBy(score => score, StringComparer.Ordinal)
 0227                    .ToList();
 228
 0229                var distributionTable = new Table()
 0230                    .Title("[bold blue]Prediction distribution[/]")
 0231                    .Border(TableBorder.Rounded)
 0232                    .AddColumn(new TableColumn("[grey]Score[/]").LeftAligned())
 0233                    .AddColumn(new TableColumn("[grey]With justification[/]").RightAligned())
 0234                    .AddColumn(new TableColumn("[grey]Without justification[/]").RightAligned());
 235
 0236                foreach (var score in scores)
 237                {
 0238                    distributions.TryGetValue(new { IncludeJustification = true, Score = score }, out var withCount);
 0239                    distributions.TryGetValue(new { IncludeJustification = false, Score = score }, out var withoutCount)
 240
 0241                    distributionTable.AddRow(
 0242                        score,
 0243                        withCount.ToString(CultureInfo.InvariantCulture),
 0244                        withoutCount.ToString(CultureInfo.InvariantCulture));
 245                }
 246
 0247                _console.Write(distributionTable);
 0248            }
 249        }
 0250        catch (Exception ex)
 251        {
 0252            logger.LogError(ex, "Error executing analyze-match comparison command");
 0253            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 0254            return 1;
 255        }
 256        finally
 257        {
 0258            loggerFactory.Dispose();
 259        }
 0260    }
 261
 0262    private sealed record ComparisonRunResult(int RunNumber, bool IncludeJustification, Prediction? Prediction, TimeSpan
 263}