< Summary

Information
Class: OpenAiIntegration.MatchPromptReconstructionService
Assembly: OpenAiIntegration
File(s): /home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/MatchPromptReconstructionService.cs
Line coverage
96%
Covered lines: 89
Uncovered lines: 3
Coverable lines: 92
Total lines: 166
Line coverage: 96.7%
Branch coverage
100%
Covered branches: 14
Total branches: 14
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ReconstructMatchPredictionPromptAsync()100%11100%
ReconstructMatchPredictionPromptAsync()100%22100%
ReconstructMatchPredictionPromptAtTimestampAsync()100%66100%
ResolveContextDocumentsAsync()100%6690.32%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/MatchPromptReconstructionService.cs

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2
 3namespace OpenAiIntegration;
 4
 5/// <summary>
 6/// Reconstructs historical match prompt inputs from stored prediction metadata and versioned context documents.
 7/// </summary>
 8public sealed class MatchPromptReconstructionService : IMatchPromptReconstructionService
 9{
 10    private readonly IPredictionRepository _predictionRepository;
 11    private readonly IContextRepository _contextRepository;
 12    private readonly IInstructionsTemplateProvider _templateProvider;
 13
 114    public MatchPromptReconstructionService(
 115        IPredictionRepository predictionRepository,
 116        IContextRepository contextRepository,
 117        IInstructionsTemplateProvider templateProvider)
 18    {
 119        _predictionRepository = predictionRepository ?? throw new ArgumentNullException(nameof(predictionRepository));
 120        _contextRepository = contextRepository ?? throw new ArgumentNullException(nameof(contextRepository));
 121        _templateProvider = templateProvider ?? throw new ArgumentNullException(nameof(templateProvider));
 122    }
 23
 24    public async Task<ReconstructedMatchPredictionPrompt?> ReconstructMatchPredictionPromptAsync(
 25        Match match,
 26        string model,
 27        string communityContext,
 28        bool includeJustification = false,
 29        CancellationToken cancellationToken = default)
 30    {
 131        return await ReconstructMatchPredictionPromptAsync(
 132            match,
 133            PredictionModelConfig.Create(model),
 134            communityContext,
 135            includeJustification,
 136            cancellationToken);
 137    }
 38
 39    public async Task<ReconstructedMatchPredictionPrompt?> ReconstructMatchPredictionPromptAsync(
 40        Match match,
 41        PredictionModelConfig modelConfig,
 42        string communityContext,
 43        bool includeJustification = false,
 44        CancellationToken cancellationToken = default)
 45    {
 146        ArgumentNullException.ThrowIfNull(match);
 147        ArgumentNullException.ThrowIfNull(modelConfig);
 148        ArgumentException.ThrowIfNullOrWhiteSpace(communityContext);
 49
 150        var predictionMetadata = await _predictionRepository.GetPredictionMetadataAsync(
 151            match,
 152            modelConfig,
 153            communityContext,
 154            cancellationToken);
 55
 156        if (predictionMetadata is null)
 57        {
 158            return null;
 59        }
 60
 161        return await ReconstructMatchPredictionPromptAtTimestampAsync(
 162            match,
 163            modelConfig.Model,
 164            communityContext,
 165            predictionMetadata.CreatedAt,
 166            predictionMetadata.ContextDocumentNames,
 167            includeJustification: includeJustification,
 168            cancellationToken: cancellationToken);
 169    }
 70
 71    public async Task<ReconstructedMatchPredictionPrompt> ReconstructMatchPredictionPromptAtTimestampAsync(
 72        Match match,
 73        string model,
 74        string communityContext,
 75        DateTimeOffset promptTimestamp,
 76        IReadOnlyList<string> requiredContextDocumentNames,
 77        IReadOnlyList<string>? optionalContextDocumentNames = null,
 78        bool includeJustification = false,
 79        CancellationToken cancellationToken = default)
 80    {
 181        ArgumentNullException.ThrowIfNull(match);
 182        ArgumentException.ThrowIfNullOrWhiteSpace(model);
 183        ArgumentException.ThrowIfNullOrWhiteSpace(communityContext);
 184        ArgumentNullException.ThrowIfNull(requiredContextDocumentNames);
 85
 186        var resolvedContextDocuments = await ResolveContextDocumentsAsync(
 187            match,
 188            communityContext,
 189            promptTimestamp,
 190            requiredContextDocumentNames,
 191            optionalContextDocumentNames ?? [],
 192            cancellationToken);
 93
 194        var (template, templatePath) = _templateProvider.LoadMatchTemplate(model, includeJustification);
 195        var systemPrompt = PredictionPromptComposer.BuildSystemPrompt(
 196            template,
 197            resolvedContextDocuments.Select(document => new DocumentContext(document.DocumentName, document.Content)));
 98
 199        return new ReconstructedMatchPredictionPrompt(
 1100            match,
 1101            model,
 1102            communityContext,
 1103            includeJustification,
 1104            promptTimestamp,
 1105            templatePath,
 1106            systemPrompt,
 1107            PredictionPromptComposer.CreateMatchJson(match),
 1108            resolvedContextDocuments.Select(document => document.DocumentName).ToArray(),
 1109            resolvedContextDocuments);
 1110    }
 111
 112    private async Task<IReadOnlyList<ResolvedContextDocumentVersion>> ResolveContextDocumentsAsync(
 113        Match match,
 114        string communityContext,
 115        DateTimeOffset promptTimestamp,
 116        IReadOnlyList<string> requiredContextDocumentNames,
 117        IReadOnlyList<string> optionalContextDocumentNames,
 118        CancellationToken cancellationToken)
 119    {
 1120        var resolvedContextDocuments = new List<ResolvedContextDocumentVersion>();
 121
 1122        foreach (var documentName in requiredContextDocumentNames)
 123        {
 1124            var contextDocument = await _contextRepository.GetContextDocumentByTimestampAsync(
 1125                documentName,
 1126                promptTimestamp,
 1127                communityContext,
 1128                cancellationToken);
 129
 1130            if (contextDocument is null)
 131            {
 0132                throw new InvalidOperationException(
 0133                    $"Failed to reconstruct prompt for {match.HomeTeam} vs {match.AwayTeam}: " +
 0134                    $"document '{documentName}' had no version at or before {promptTimestamp:O}.");
 135            }
 136
 1137            resolvedContextDocuments.Add(new ResolvedContextDocumentVersion(
 1138                contextDocument.DocumentName,
 1139                contextDocument.Version,
 1140                contextDocument.CreatedAt,
 1141                contextDocument.Content));
 1142        }
 143
 1144        foreach (var documentName in optionalContextDocumentNames)
 145        {
 1146            var contextDocument = await _contextRepository.GetContextDocumentByTimestampAsync(
 1147                documentName,
 1148                promptTimestamp,
 1149                communityContext,
 1150                cancellationToken);
 151
 1152            if (contextDocument is null)
 153            {
 154                continue;
 155            }
 156
 1157            resolvedContextDocuments.Add(new ResolvedContextDocumentVersion(
 1158                contextDocument.DocumentName,
 1159                contextDocument.Version,
 1160                contextDocument.CreatedAt,
 1161                contextDocument.Content));
 162        }
 163
 1164        return resolvedContextDocuments;
 1165    }
 166}