< Summary

Information
Class: OpenAiIntegration.MatchPromptReconstructionService
Assembly: OpenAiIntegration
File(s): /home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/MatchPromptReconstructionService.cs
Line coverage
96%
Covered lines: 82
Uncovered lines: 3
Coverable lines: 85
Total lines: 151
Line coverage: 96.4%
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%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        ArgumentNullException.ThrowIfNull(match);
 132        ArgumentException.ThrowIfNullOrWhiteSpace(model);
 133        ArgumentException.ThrowIfNullOrWhiteSpace(communityContext);
 34
 135        var predictionMetadata = await _predictionRepository.GetPredictionMetadataAsync(
 136            match,
 137            model,
 138            communityContext,
 139            cancellationToken);
 40
 141        if (predictionMetadata is null)
 42        {
 143            return null;
 44        }
 45
 146        return await ReconstructMatchPredictionPromptAtTimestampAsync(
 147            match,
 148            model,
 149            communityContext,
 150            predictionMetadata.CreatedAt,
 151            predictionMetadata.ContextDocumentNames,
 152            includeJustification: includeJustification,
 153            cancellationToken: cancellationToken);
 154    }
 55
 56    public async Task<ReconstructedMatchPredictionPrompt> ReconstructMatchPredictionPromptAtTimestampAsync(
 57        Match match,
 58        string model,
 59        string communityContext,
 60        DateTimeOffset promptTimestamp,
 61        IReadOnlyList<string> requiredContextDocumentNames,
 62        IReadOnlyList<string>? optionalContextDocumentNames = null,
 63        bool includeJustification = false,
 64        CancellationToken cancellationToken = default)
 65    {
 166        ArgumentNullException.ThrowIfNull(match);
 167        ArgumentException.ThrowIfNullOrWhiteSpace(model);
 168        ArgumentException.ThrowIfNullOrWhiteSpace(communityContext);
 169        ArgumentNullException.ThrowIfNull(requiredContextDocumentNames);
 70
 171        var resolvedContextDocuments = await ResolveContextDocumentsAsync(
 172            match,
 173            communityContext,
 174            promptTimestamp,
 175            requiredContextDocumentNames,
 176            optionalContextDocumentNames ?? [],
 177            cancellationToken);
 78
 179        var (template, templatePath) = _templateProvider.LoadMatchTemplate(model, includeJustification);
 180        var systemPrompt = PredictionPromptComposer.BuildSystemPrompt(
 181            template,
 182            resolvedContextDocuments.Select(document => new DocumentContext(document.DocumentName, document.Content)));
 83
 184        return new ReconstructedMatchPredictionPrompt(
 185            match,
 186            model,
 187            communityContext,
 188            includeJustification,
 189            promptTimestamp,
 190            templatePath,
 191            systemPrompt,
 192            PredictionPromptComposer.CreateMatchJson(match),
 193            resolvedContextDocuments.Select(document => document.DocumentName).ToArray(),
 194            resolvedContextDocuments);
 195    }
 96
 97    private async Task<IReadOnlyList<ResolvedContextDocumentVersion>> ResolveContextDocumentsAsync(
 98        Match match,
 99        string communityContext,
 100        DateTimeOffset promptTimestamp,
 101        IReadOnlyList<string> requiredContextDocumentNames,
 102        IReadOnlyList<string> optionalContextDocumentNames,
 103        CancellationToken cancellationToken)
 104    {
 1105        var resolvedContextDocuments = new List<ResolvedContextDocumentVersion>();
 106
 1107        foreach (var documentName in requiredContextDocumentNames)
 108        {
 1109            var contextDocument = await _contextRepository.GetContextDocumentByTimestampAsync(
 1110                documentName,
 1111                promptTimestamp,
 1112                communityContext,
 1113                cancellationToken);
 114
 1115            if (contextDocument is null)
 116            {
 0117                throw new InvalidOperationException(
 0118                    $"Failed to reconstruct prompt for {match.HomeTeam} vs {match.AwayTeam}: " +
 0119                    $"document '{documentName}' had no version at or before {promptTimestamp:O}.");
 120            }
 121
 1122            resolvedContextDocuments.Add(new ResolvedContextDocumentVersion(
 1123                contextDocument.DocumentName,
 1124                contextDocument.Version,
 1125                contextDocument.CreatedAt,
 1126                contextDocument.Content));
 1127        }
 128
 1129        foreach (var documentName in optionalContextDocumentNames)
 130        {
 1131            var contextDocument = await _contextRepository.GetContextDocumentByTimestampAsync(
 1132                documentName,
 1133                promptTimestamp,
 1134                communityContext,
 1135                cancellationToken);
 136
 1137            if (contextDocument is null)
 138            {
 139                continue;
 140            }
 141
 1142            resolvedContextDocuments.Add(new ResolvedContextDocumentVersion(
 1143                contextDocument.DocumentName,
 1144                contextDocument.Version,
 1145                contextDocument.CreatedAt,
 1146                contextDocument.Content));
 147        }
 148
 1149        return resolvedContextDocuments;
 1150    }
 151}