< Summary

Information
Class: Orchestrator.Commands.Observability.ExportExperimentItem.ExportExperimentItemCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/ExportExperimentItem/ExportExperimentItemCommand.cs
Line coverage
86%
Covered lines: 112
Uncovered lines: 18
Coverable lines: 130
Total lines: 204
Line coverage: 86.1%
Branch coverage
64%
Covered branches: 22
Total branches: 34
Branch coverage: 64.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
ExecuteAsync()65.38%332678.08%
BuildExport(...)75%44100%
ResolveOutputPath(...)50%3250%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Observability/ExportExperimentItem/ExportExperimentItemCommand.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Text.Json;
 3using EHonda.KicktippAi.Core;
 4using Microsoft.Extensions.Logging;
 5using OpenAiIntegration;
 6using Orchestrator.Commands.Observability;
 7using Orchestrator.Infrastructure.Factories;
 8using Spectre.Console;
 9using Spectre.Console.Cli;
 10
 11namespace Orchestrator.Commands.Observability.ExportExperimentItem;
 12
 13public sealed class ExportExperimentItemCommand : AsyncCommand<ExportExperimentItemSettings>
 14{
 115    private static readonly JsonSerializerOptions OutputJsonOptions = new()
 116    {
 117        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 118        WriteIndented = true
 119    };
 20
 21    private readonly IAnsiConsole _console;
 22    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 23    private readonly ILogger<ExportExperimentItemCommand> _logger;
 24
 125    public ExportExperimentItemCommand(
 126        IAnsiConsole console,
 127        IFirebaseServiceFactory firebaseServiceFactory,
 128        ILogger<ExportExperimentItemCommand> logger)
 29    {
 130        _console = console;
 131        _firebaseServiceFactory = firebaseServiceFactory;
 132        _logger = logger;
 133    }
 34
 35    protected override async Task<int> ExecuteAsync(CommandContext context, ExportExperimentItemSettings settings, Cance
 36    {
 37        try
 38        {
 139            _console.MarkupLine($"[green]Exporting experiment item for:[/] [yellow]{Markup.Escape(settings.HomeTeam)}[/]
 40
 141            var predictionRepository = _firebaseServiceFactory.CreatePredictionRepository();
 142            var contextRepository = _firebaseServiceFactory.CreateContextRepository();
 143            var matchOutcomeRepository = _firebaseServiceFactory.CreateMatchOutcomeRepository();
 44
 145            var reconstructionService = new MatchPromptReconstructionService(
 146                predictionRepository,
 147                contextRepository,
 148                new InstructionsTemplateProvider(PromptsFileProvider.Create()));
 49
 150            var evaluationTime = EvaluationTimeParser.ParseOrNull(settings.EvaluationTime);
 151            var evaluationPolicy = EvaluationTimestampPolicyParser.ParseOrNull(
 152                settings.EvaluationPolicyKind,
 153                settings.EvaluationPolicyOffset);
 154            var reconstructAtTimestamp = evaluationTime is not null || evaluationPolicy is not null;
 155            var modelConfig = PredictionModelConfig.Create(settings.Model, settings.ReasoningEffort);
 56
 157            var storedMatch = await predictionRepository.GetStoredMatchAsync(
 158                settings.HomeTeam,
 159                settings.AwayTeam,
 160                settings.Matchday!.Value,
 161                reconstructAtTimestamp ? null : modelConfig,
 162                reconstructAtTimestamp ? null : settings.CommunityContext);
 63
 164            if (storedMatch is null)
 65            {
 166                _console.MarkupLine($"[red]Stored match not found on matchday {settings.Matchday}:[/] {Markup.Escape(set
 167                return 1;
 68            }
 69
 170            var promptMatch = ExperimentArtifactSupport.RehydrateForPromptOutput(storedMatch);
 171            var resolvedEvaluationTime = evaluationTime
 172                ?? (evaluationPolicy is null ? null : EvaluationTimestampResolver.Resolve(promptMatch, evaluationPolicy)
 73
 74            ReconstructedMatchPredictionPrompt? reconstructedPrompt;
 175            if (resolvedEvaluationTime is null)
 76            {
 177                reconstructedPrompt = await reconstructionService.ReconstructMatchPredictionPromptAsync(
 178                    promptMatch,
 179                    modelConfig,
 180                    settings.CommunityContext,
 181                    settings.WithJustification);
 82            }
 83            else
 84            {
 085                var selection = MatchContextDocumentCatalog.ForMatch(
 086                    promptMatch.HomeTeam,
 087                    promptMatch.AwayTeam,
 088                    settings.CommunityContext);
 89
 090                reconstructedPrompt = await reconstructionService.ReconstructMatchPredictionPromptAtTimestampAsync(
 091                    promptMatch,
 092                    settings.Model,
 093                    settings.CommunityContext,
 094                    resolvedEvaluationTime.Value,
 095                    selection.RequiredDocumentNames,
 096                    selection.OptionalDocumentNames,
 097                    settings.WithJustification);
 98            }
 99
 1100            if (reconstructedPrompt is null)
 101            {
 0102                _console.MarkupLine("[red]No stored prediction metadata found for that match/model/community combination
 0103                return 1;
 104            }
 105
 1106            var outcomes = await matchOutcomeRepository.GetMatchdayOutcomesAsync(
 1107                settings.Matchday.Value,
 1108                settings.CommunityContext);
 109
 1110            var outcome = outcomes.FirstOrDefault(candidate =>
 1111                string.Equals(candidate.HomeTeam, settings.HomeTeam, StringComparison.OrdinalIgnoreCase) &&
 1112                string.Equals(candidate.AwayTeam, settings.AwayTeam, StringComparison.OrdinalIgnoreCase));
 113
 1114            if (outcome is null)
 115            {
 0116                _console.MarkupLine("[red]No persisted match outcome was found for the selected match.[/]");
 0117                return 1;
 118            }
 119
 1120            if (!outcome.HasOutcome || outcome.HomeGoals is null || outcome.AwayGoals is null)
 121            {
 1122                _console.MarkupLine("[red]The selected match does not have a completed persisted outcome yet.[/]");
 1123                return 1;
 124            }
 125
 1126            var export = BuildExport(reconstructedPrompt, outcome);
 1127            var outputPath = ResolveOutputPath(settings, export.DatasetItem.Metadata);
 1128            Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
 129
 1130            await File.WriteAllTextAsync(
 1131                outputPath,
 1132                JsonSerializer.Serialize(export, OutputJsonOptions));
 133
 1134            _console.MarkupLine($"[green]Wrote experiment item:[/] [yellow]{Markup.Escape(outputPath)}[/]");
 1135            _console.MarkupLine($"[blue]Dataset item id:[/] {Markup.Escape(export.DatasetItem.Id)}");
 1136            _console.MarkupLine($"[blue]{(resolvedEvaluationTime is null ? "Prediction timestamp" : "Reconstruction time
 1137            _console.MarkupLine($"[blue]Outcome:[/] {outcome.HomeGoals}:{outcome.AwayGoals} ({outcome.Availability})");
 138
 1139            return 0;
 140        }
 1141        catch (Exception ex)
 142        {
 1143            _logger.LogError(ex, "Error exporting experiment item");
 1144            _console.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
 1145            return 1;
 146        }
 1147    }
 148
 149    private static ExportedExperimentItem BuildExport(
 150        ReconstructedMatchPredictionPrompt reconstructedPrompt,
 151        PersistedMatchOutcome outcome)
 152    {
 1153        using var matchJsonDocument = JsonDocument.Parse(reconstructedPrompt.MatchJson);
 1154        var tippSpielId = outcome.TippSpielId ?? throw new InvalidOperationException(
 1155            $"Persisted outcome for {outcome.HomeTeam} vs {outcome.AwayTeam} is missing tippspielId.");
 156
 1157        var metadata = new MatchExperimentMetadata(
 1158            reconstructedPrompt.CommunityContext,
 1159            outcome.Competition,
 1160            reconstructedPrompt.Match.Matchday,
 1161            reconstructedPrompt.Match.HomeTeam,
 1162            reconstructedPrompt.Match.AwayTeam,
 1163            tippSpielId,
 1164            reconstructedPrompt.Model,
 1165            reconstructedPrompt.IncludeJustification,
 1166            reconstructedPrompt.PromptTimestamp,
 1167            reconstructedPrompt.PromptTemplatePath,
 1168            reconstructedPrompt.ContextDocumentNames,
 1169            reconstructedPrompt.ResolvedContextDocuments
 1170                .Select(document => new MatchExperimentResolvedContextDocument(
 1171                    document.DocumentName,
 1172                    document.Version,
 1173                    document.CreatedAt))
 1174                .ToList()
 1175                .AsReadOnly(),
 1176            new MatchExperimentOutcome(
 1177                outcome.HomeGoals!.Value,
 1178                outcome.AwayGoals!.Value));
 179
 1180        return new ExportedExperimentItem(
 1181            new MatchExperimentDatasetItem(
 1182                ExperimentArtifactSupport.BuildHostedDatasetItemId(outcome.Competition, outcome.CommunityContext, tippSp
 1183                matchJsonDocument.RootElement.Clone(),
 1184                new MatchExperimentExpectedOutput(
 1185                    outcome.HomeGoals!.Value,
 1186                    outcome.AwayGoals!.Value,
 1187                    outcome.Availability.ToString()),
 1188                metadata),
 1189            new MatchExperimentRunnerPayload(
 1190                reconstructedPrompt.SystemPrompt,
 1191                reconstructedPrompt.MatchJson));
 1192    }
 193
 194    private static string ResolveOutputPath(ExportExperimentItemSettings settings, MatchExperimentMetadata metadata)
 195    {
 1196        if (!string.IsNullOrWhiteSpace(settings.OutputPath))
 197        {
 1198            return Path.GetFullPath(settings.OutputPath);
 199        }
 200
 0201        var fileName = $"{metadata.Matchday:00}-{ExperimentArtifactSupport.Slugify(metadata.HomeTeam)}-vs-{ExperimentArt
 0202        return Path.GetFullPath(Path.Combine("artifacts", "langfuse-experiments", "items", fileName));
 203    }
 204}