< Summary

Information
Class: Orchestrator.Commands.Observability.AnalyzeMatch.AnalyzeMatchDetailedCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/AnalyzeMatch/AnalyzeMatchDetailedCommand.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 148
Coverable lines: 148
Total lines: 267
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 54
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%702260%
FormatCurrencyValue()100%210%
FormatCurrencyOptional()0%620%
FormatDurationValue()100%210%
FormatDurationOptional()0%620%
BuildSummary()0%420200%
ExecuteRunsAsync()0%2040%
<ExecuteAsync()100%210%
.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
 025    public AnalyzeMatchDetailedCommand(
 026        IAnsiConsole console,
 027        IFirebaseServiceFactory firebaseServiceFactory,
 028        IKicktippClientFactory kicktippClientFactory,
 029        IOpenAiServiceFactory openAiServiceFactory)
 30    {
 031        _console = console;
 032        _firebaseServiceFactory = firebaseServiceFactory;
 033        _kicktippClientFactory = kicktippClientFactory;
 034        _openAiServiceFactory = openAiServiceFactory;
 035    }
 36
 37    public override async Task<int> ExecuteAsync(CommandContext context, AnalyzeMatchDetailedSettings settings)
 38    {
 039        var loggerFactory = AnalyzeMatchCommandHelpers.CreateLoggerFactory(settings.Debug);
 040        var logger = loggerFactory.CreateLogger<AnalyzeMatchDetailedCommand>();
 41
 42        try
 43        {
 044            var validation = settings.Validate();
 045            if (!validation.Successful)
 46            {
 047                _console.MarkupLine($"[red]Error:[/] {validation.Message}");
 048                return 1;
 49            }
 50
 051            var predictionService = _openAiServiceFactory.CreatePredictionService(settings.Model);
 052            var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 053            var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 054            var kicktippClient = _kicktippClientFactory.CreateClient();
 55
 056            var communityContext = settings.CommunityContext!;
 57
 058            _console.MarkupLine($"[green]Analyze match initialized with model:[/] [yellow]{settings.Model}[/]");
 059            _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 060            _console.MarkupLine($"[blue]Runs:[/] [yellow]{settings.Runs}[/]");
 61
 062            if (settings.Debug)
 63            {
 064                _console.MarkupLine("[dim]Debug logging enabled[/]");
 65            }
 66
 067            var match = await AnalyzeMatchCommandHelpers.ResolveMatchAsync(settings, kicktippClient, logger, communityCo
 068            if (match == null)
 69            {
 070                _console.MarkupLine("[red]Failed to resolve match details. Aborting.[/]");
 071                return 1;
 72            }
 73
 074            var contextDocuments = new List<DocumentContext>();
 75
 076            if (contextRepository != null)
 77            {
 078                var contextDocumentInfos = await AnalyzeMatchCommandHelpers.GetMatchContextDocumentsAsync(
 079                    contextRepository,
 080                    match.HomeTeam,
 081                    match.AwayTeam,
 082                    communityContext,
 083                    settings.Verbose);
 84
 085                contextDocuments = contextDocumentInfos.Select(info => info.Document).ToList();
 86
 087                if (contextDocumentInfos.Any())
 88                {
 089                    _console.MarkupLine("[dim]Loaded context documents:[/]");
 090                    foreach (var info in contextDocumentInfos)
 91                    {
 092                        _console.MarkupLine($"[grey]  • {info.Document.Name}[/] [dim](v{info.Version})[/]");
 93
 094                        if (settings.ShowContextDocuments)
 95                        {
 096                            var lines = info.Document.Content.Split('\n');
 097                            foreach (var line in lines.Take(10))
 98                            {
 099                                _console.MarkupLine($"[grey]      {line.EscapeMarkup()}[/]");
 100                            }
 101
 0102                            if (lines.Length > 10)
 103                            {
 0104                                _console.MarkupLine($"[dim]      ... ({lines.Length - 10} more lines) ...[/]");
 105                            }
 106                        }
 107                    }
 108                }
 109                else
 110                {
 0111                    _console.MarkupLine("[yellow]No context documents retrieved; proceeding without additional context[/
 112                }
 113            }
 114            else
 115            {
 0116                _console.MarkupLine("[yellow]Context repository not configured. Proceeding without context documents.[/]
 117            }
 118
 0119            tokenUsageTracker.Reset();
 120
 0121            var predictions = new List<Prediction>();
 0122            var runMetrics = new List<RunMetric>();
 0123            var enableLiveEstimates = !settings.NoLiveEstimates;
 124
 0125            string FormatCurrencyValue(decimal value) => $"${value.ToString("F4", CultureInfo.InvariantCulture)}";
 0126            string FormatCurrencyOptional(decimal? value) => value.HasValue ? FormatCurrencyValue(value.Value) : "n/a";
 127
 0128            string FormatDurationValue(TimeSpan value) => value.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCultur
 0129            string FormatDurationOptional(TimeSpan? value) => value.HasValue ? FormatDurationValue(value.Value) : "n/a";
 130
 131            IRenderable BuildSummary()
 132            {
 0133                var completedRuns = runMetrics.Count;
 0134                var remainingRuns = Math.Max(settings.Runs - completedRuns, 0);
 0135                var successfulRuns = runMetrics.Where(metric => metric.Success).ToList();
 0136                var costValues = successfulRuns
 0137                    .Where(metric => metric.Cost.HasValue)
 0138                    .Select(metric => metric.Cost!.Value)
 0139                    .ToList();
 140
 0141                var totalCostSoFar = costValues.Aggregate(0m, (sum, value) => sum + value);
 0142                decimal? averageCost = costValues.Count > 0 ? totalCostSoFar / costValues.Count : (decimal?)null;
 0143                decimal? projectedCost = averageCost.HasValue ? averageCost.Value * settings.Runs : (decimal?)null;
 144
 0145                var totalDuration = runMetrics.Aggregate(TimeSpan.Zero, (current, metric) => current + metric.Duration);
 0146                TimeSpan? averageDuration = runMetrics.Count > 0
 0147                    ? TimeSpan.FromTicks(totalDuration.Ticks / runMetrics.Count)
 0148                    : (TimeSpan?)null;
 0149                TimeSpan? estimatedRemaining = averageDuration.HasValue && remainingRuns > 0
 0150                    ? TimeSpan.FromTicks(averageDuration.Value.Ticks * remainingRuns)
 0151                    : (TimeSpan?)null;
 152
 0153                var table = new Table()
 0154                    .Title("[bold yellow]Live Estimates[/]")
 0155                    .Border(TableBorder.Rounded)
 0156                    .AddColumn(new TableColumn("[grey]Metric[/]").LeftAligned())
 0157                    .AddColumn(new TableColumn("[grey]Value[/]").LeftAligned());
 158
 0159                table.AddRow("Completed runs", $"{completedRuns}/{settings.Runs}");
 0160                table.AddRow("Successful predictions", $"{successfulRuns.Count}/{settings.Runs}");
 0161                table.AddRow("Total cost so far", FormatCurrencyValue(totalCostSoFar));
 0162                table.AddRow("Average cost", FormatCurrencyOptional(averageCost));
 0163                table.AddRow("Projected total cost", FormatCurrencyOptional(projectedCost));
 0164                table.AddRow("Average run time", FormatDurationOptional(averageDuration));
 0165                table.AddRow("Estimated remaining time", FormatDurationOptional(estimatedRemaining));
 0166                table.AddRow("Elapsed time", FormatDurationValue(totalDuration));
 167
 0168                return table;
 169            }
 170
 0171            Action refreshSummary = () => { };
 172
 173            async Task ExecuteRunsAsync()
 174            {
 0175                for (var run = 1; run <= settings.Runs; run++)
 176                {
 0177                    var stopwatch = Stopwatch.StartNew();
 178
 0179                    _console.MarkupLine($"[cyan]\nRun {run}/{settings.Runs}[/]");
 180
 0181                    var prediction = await predictionService.PredictMatchAsync(
 0182                        match,
 0183                        contextDocuments,
 0184                        includeJustification: true);
 185
 0186                    stopwatch.Stop();
 187
 0188                    if (prediction == null)
 189                    {
 0190                        _console.MarkupLine("[red]  ✗ Prediction failed[/]");
 0191                        runMetrics.Add(new RunMetric(run, stopwatch.Elapsed, false, null));
 0192                        refreshSummary();
 0193                        continue;
 194                    }
 195
 0196                    predictions.Add(prediction);
 197
 0198                    var lastCost = tokenUsageTracker.GetLastCost();
 0199                    var usageSummary = tokenUsageTracker.GetLastUsageCompactSummary();
 200
 0201                    runMetrics.Add(new RunMetric(run, stopwatch.Elapsed, true, lastCost));
 202
 0203                    _console.MarkupLine($"[green]  ✓ Prediction:[/] [yellow]{prediction.HomeGoals}:{prediction.AwayGoals
 204
 0205                    var justificationWriter = new JustificationConsoleWriter(_console);
 0206                    justificationWriter.WriteJustification(
 0207                        prediction.Justification,
 0208                        "[cyan]  ↳ Justification:[/]",
 0209                        "      ",
 0210                        "[yellow]  ↳ Justification: no explanation returned by model[/]");
 211
 0212                    _console.MarkupLine($"[magenta]  ↳ Cost:[/] [cyan]{FormatCurrencyValue(lastCost)}[/] [grey]({usageSu
 213
 0214                    refreshSummary();
 0215                }
 216
 0217                refreshSummary();
 0218            }
 219
 0220            if (enableLiveEstimates)
 221            {
 0222                await _console.Live(BuildSummary())
 0223                    .AutoClear(false)
 0224                    .StartAsync(async ctx =>
 0225                    {
 0226                        refreshSummary = () =>
 0227                        {
 0228                            ctx.UpdateTarget(BuildSummary());
 0229                            ctx.Refresh();
 0230                        };
 0231
 0232                        refreshSummary();
 0233                        await ExecuteRunsAsync();
 0234                    });
 235            }
 236            else
 237            {
 0238                await ExecuteRunsAsync();
 0239                _console.Write(BuildSummary());
 240            }
 241
 0242            if (predictions.Any())
 243            {
 0244                _console.MarkupLine($"\n[blue]Total runs with predictions:[/] [yellow]{predictions.Count}/{settings.Runs
 0245                _console.MarkupLine($"[blue]Total cost:[/] [yellow]{FormatCurrencyValue(tokenUsageTracker.GetTotalCost()
 246            }
 247            else
 248            {
 0249                _console.MarkupLine("[red]No successful predictions generated.[/]");
 250            }
 251
 0252            return 0;
 253        }
 0254        catch (Exception ex)
 255        {
 0256            logger.LogError(ex, "Error executing analyze-match command");
 0257            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 0258            return 1;
 259        }
 260        finally
 261        {
 0262            loggerFactory.Dispose();
 263        }
 0264    }
 265
 0266    private sealed record RunMetric(int RunNumber, TimeSpan Duration, bool Success, decimal? Cost);
 267}