< Summary

Information
Class: OpenAiIntegration.PredictionService
Assembly: OpenAiIntegration
File(s): /home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/PredictionService.cs
Line coverage
96%
Covered lines: 364
Uncovered lines: 12
Coverable lines: 376
Total lines: 667
Line coverage: 96.8%
Branch coverage
75%
Covered branches: 80
Total branches: 106
Branch coverage: 75.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Linq;
 3using System.Text.Json;
 4using System.Text.Json.Serialization;
 5using EHonda.KicktippAi.Core;
 6using Microsoft.Extensions.Logging;
 7using OpenAI.Chat;
 8
 9namespace OpenAiIntegration;
 10
 11/// <summary>
 12/// Service for predicting match outcomes using OpenAI models
 13/// </summary>
 14public class PredictionService : IPredictionService
 15{
 16    private readonly ChatClient _chatClient;
 17    private readonly ILogger<PredictionService> _logger;
 18    private readonly ICostCalculationService _costCalculationService;
 19    private readonly ITokenUsageTracker _tokenUsageTracker;
 20    private readonly IInstructionsTemplateProvider _templateProvider;
 21    private readonly string _model;
 22    private readonly string _instructionsTemplate;
 23    private readonly string _instructionsTemplateWithJustification;
 24    private readonly string _bonusInstructionsTemplate;
 25    private readonly string _matchPromptPath;
 26    private readonly string _matchPromptPathWithJustification;
 27    private readonly string _bonusPromptPath;
 28
 129    public PredictionService(
 130        ChatClient chatClient,
 131        ILogger<PredictionService> logger,
 132        ICostCalculationService costCalculationService,
 133        ITokenUsageTracker tokenUsageTracker,
 134        IInstructionsTemplateProvider templateProvider,
 135        string model)
 36    {
 137        _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient));
 138        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 139        _costCalculationService = costCalculationService ?? throw new ArgumentNullException(nameof(costCalculationServic
 140        _tokenUsageTracker = tokenUsageTracker ?? throw new ArgumentNullException(nameof(tokenUsageTracker));
 141        _templateProvider = templateProvider ?? throw new ArgumentNullException(nameof(templateProvider));
 142        _model = model ?? throw new ArgumentNullException(nameof(model));
 43
 144        var (matchTemplate, matchPath) = _templateProvider.LoadMatchTemplate(_model, includeJustification: false);
 145        var (matchJustificationTemplate, matchJustificationPath) = _templateProvider.LoadMatchTemplate(_model, includeJu
 146        var (bonusTemplate, bonusPath) = _templateProvider.LoadBonusTemplate(_model);
 47
 148        _instructionsTemplate = matchTemplate;
 149        _instructionsTemplateWithJustification = matchJustificationTemplate;
 150        _bonusInstructionsTemplate = bonusTemplate;
 151        _matchPromptPath = matchPath;
 152        _matchPromptPathWithJustification = matchJustificationPath;
 153        _bonusPromptPath = bonusPath;
 154    }
 55
 56    public async Task<Prediction?> PredictMatchAsync(
 57        Match match,
 58        IEnumerable<DocumentContext> contextDocuments,
 59        bool includeJustification = false,
 60        CancellationToken cancellationToken = default)
 61    {
 162        _logger.LogInformation("Generating prediction for match: {HomeTeam} vs {AwayTeam} at {StartTime}",
 163            match.HomeTeam, match.AwayTeam, match.StartsAt);
 64
 65        try
 66        {
 67            // Build the instructions by combining template with context
 168            var instructions = BuildInstructions(contextDocuments, includeJustification);
 69
 70            // Create match JSON
 171            var matchJson = CreateMatchJson(match);
 72
 173            _logger.LogDebug("Instructions length: {InstructionsLength} characters", instructions.Length);
 174            _logger.LogDebug("Context documents: {ContextCount}", contextDocuments.Count());
 175            _logger.LogDebug("Match JSON: {MatchJson}", matchJson);
 76
 77            // Create messages for the chat completion
 178            var messages = new List<ChatMessage>
 179            {
 180                new SystemChatMessage(instructions),
 181                new UserChatMessage(matchJson)
 182            };
 83
 184            _logger.LogDebug("Calling OpenAI API for prediction");
 85
 86            // Call OpenAI with structured output format
 187            var response = await _chatClient.CompleteChatAsync(
 188                messages,
 189                new ChatCompletionOptions
 190                {
 191                    MaxOutputTokenCount = 10_000, // Safeguard against high costs
 192                    ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
 193                        jsonSchemaFormatName: "match_prediction",
 194                        jsonSchema: BinaryData.FromBytes(BuildPredictionJsonSchema(includeJustification)),
 195                        jsonSchemaIsStrict: true)
 196                },
 197                cancellationToken);
 98
 99            // Parse the structured response
 1100            var predictionJson = response.Value.Content[0].Text;
 1101            _logger.LogDebug("Received prediction JSON: {PredictionJson}", predictionJson);
 102
 1103            var prediction = ParsePrediction(predictionJson);
 104
 1105            _logger.LogInformation("Prediction generated: {HomeGoals}-{AwayGoals} for {HomeTeam} vs {AwayTeam}",
 1106                prediction.HomeGoals, prediction.AwayGoals, match.HomeTeam, match.AwayTeam);
 107
 108            // Log token usage and cost breakdown
 1109            var usage = response.Value.Usage;
 1110            _logger.LogDebug("Token usage - Input: {InputTokens}, Output: {OutputTokens}, Total: {TotalTokens}",
 1111                usage.InputTokenCount, usage.OutputTokenCount, usage.TotalTokenCount);
 112
 113            // Add usage to tracker
 1114            _tokenUsageTracker.AddUsage(_model, usage);
 115
 116            // Calculate and log costs
 1117            _costCalculationService.LogCostBreakdown(_model, usage);
 118
 1119            return prediction;
 120        }
 1121        catch (Exception ex)
 122        {
 1123            _logger.LogError(ex, "Error generating prediction for match: {HomeTeam} vs {AwayTeam}",
 1124                match.HomeTeam, match.AwayTeam);
 1125            Console.Error.WriteLine($"Prediction error for {match.HomeTeam} vs {match.AwayTeam}: {ex.Message}");
 126
 1127            return null;
 128        }
 1129    }
 130
 131    public async Task<BonusPrediction?> PredictBonusQuestionAsync(
 132        BonusQuestion bonusQuestion,
 133        IEnumerable<DocumentContext> contextDocuments,
 134        CancellationToken cancellationToken = default)
 135    {
 1136        _logger.LogInformation("Generating prediction for bonus question: {QuestionText}", bonusQuestion.Text);
 137
 138        try
 139        {
 140            // Build the instructions by combining template with context
 1141            var instructions = BuildBonusInstructions(contextDocuments);
 142
 143            // Create bonus question JSON
 1144            var questionJson = CreateBonusQuestionJson(bonusQuestion);
 145
 1146            _logger.LogDebug("Instructions length: {InstructionsLength} characters", instructions.Length);
 1147            _logger.LogDebug("Context documents: {ContextCount}", contextDocuments.Count());
 1148            _logger.LogDebug("Question JSON: {QuestionJson}", questionJson);
 149
 150            // Create messages for the chat completion
 1151            var messages = new List<ChatMessage>
 1152            {
 1153                new SystemChatMessage(instructions),
 1154                new UserChatMessage(questionJson)
 1155            };
 156
 1157            _logger.LogDebug("Calling OpenAI API for bonus prediction");
 158
 159            // Create JSON schema based on the question
 1160            var jsonSchema = CreateSingleBonusPredictionJsonSchema(bonusQuestion);
 161
 162            // Call OpenAI with structured output format
 1163            var response = await _chatClient.CompleteChatAsync(
 1164                messages,
 1165                new ChatCompletionOptions
 1166                {
 1167                    MaxOutputTokenCount = 10_000, // Standard limit for single question
 1168                    ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
 1169                        jsonSchemaFormatName: "bonus_prediction",
 1170                        jsonSchema: BinaryData.FromBytes(jsonSchema),
 1171                        jsonSchemaIsStrict: true)
 1172                },
 1173                cancellationToken);
 174
 175            // Parse the structured response
 1176            var predictionJson = response.Value.Content[0].Text;
 1177            _logger.LogDebug("Received bonus prediction JSON: {PredictionJson}", predictionJson);
 178
 1179            var prediction = ParseSingleBonusPrediction(predictionJson, bonusQuestion);
 180
 1181            if (prediction != null)
 182            {
 1183                _logger.LogInformation("Generated prediction for bonus question: {SelectedOptions}",
 1184                    string.Join(", ", prediction.SelectedOptionIds));
 185            }
 186
 187            // Log token usage and cost breakdown
 1188            var usage = response.Value.Usage;
 1189            _logger.LogDebug("Token usage - Input: {InputTokens}, Output: {OutputTokens}, Total: {TotalTokens}",
 1190                usage.InputTokenCount, usage.OutputTokenCount, usage.TotalTokenCount);
 191
 192            // Add usage to tracker
 1193            _tokenUsageTracker.AddUsage(_model, usage);
 194
 195            // Calculate and log costs
 1196            _costCalculationService.LogCostBreakdown(_model, usage);
 197
 1198            return prediction;
 199        }
 1200        catch (Exception ex)
 201        {
 1202            _logger.LogError(ex, "Error generating bonus prediction for question: {QuestionText}", bonusQuestion.Text);
 1203            return null;
 204        }
 1205    }
 206
 207    private string BuildInstructions(IEnumerable<DocumentContext> contextDocuments, bool includeJustification)
 208    {
 1209        var instructions = includeJustification
 1210            ? _instructionsTemplateWithJustification
 1211            : _instructionsTemplate;
 212
 1213        var contextList = contextDocuments.ToList();
 1214        if (contextList.Any())
 215        {
 1216            var contextSection = "\n";
 1217            foreach (var doc in contextList)
 218            {
 1219                contextSection += "---\n";
 1220                contextSection += $"{doc.Name}\n\n";
 1221                contextSection += $"{doc.Content}\n";
 222            }
 1223            contextSection += "---";
 224
 1225            instructions += contextSection;
 226
 1227            _logger.LogDebug("Added {ContextCount} context documents to instructions", contextList.Count);
 228        }
 229        else
 230        {
 1231            _logger.LogDebug("No context documents provided");
 232        }
 233
 1234        return instructions;
 235    }
 236
 237    private static string CreateMatchJson(Match match)
 238    {
 1239        return JsonSerializer.Serialize(new
 1240        {
 1241            homeTeam = match.HomeTeam,
 1242            awayTeam = match.AwayTeam,
 1243            startsAt = match.StartsAt.ToString()
 1244        }, new JsonSerializerOptions
 1245        {
 1246            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
 1247        });
 248    }
 249
 250    private static byte[] BuildPredictionJsonSchema(bool includeJustification)
 251    {
 1252        var properties = new Dictionary<string, object?>
 1253        {
 1254            ["home"] = new Dictionary<string, object?>
 1255            {
 1256                ["type"] = "integer",
 1257                ["description"] = "Predicted goals for the home team"
 1258            },
 1259            ["away"] = new Dictionary<string, object?>
 1260            {
 1261                ["type"] = "integer",
 1262                ["description"] = "Predicted goals for the away team"
 1263            }
 1264        };
 265
 1266        var required = new List<string> { "home", "away" };
 267
 1268        if (includeJustification)
 269        {
 1270            var mostValuableContextSourceItem = new Dictionary<string, object?>
 1271            {
 1272                ["type"] = "object",
 1273                ["properties"] = new Dictionary<string, object?>
 1274                {
 1275                    ["documentName"] = new Dictionary<string, object?>
 1276                    {
 1277                        ["type"] = "string",
 1278                        ["description"] = "Name of the context document referenced"
 1279                    },
 1280                    ["details"] = new Dictionary<string, object?>
 1281                    {
 1282                        ["type"] = "string",
 1283                        ["description"] = "Brief summary of why the document or parts of it were useful"
 1284                    }
 1285                },
 1286                ["required"] = new[] { "documentName", "details" },
 1287                ["additionalProperties"] = false
 1288            };
 289
 1290            var leastValuableContextSourceItem = new Dictionary<string, object?>
 1291            {
 1292                ["type"] = "object",
 1293                ["properties"] = new Dictionary<string, object?>
 1294                {
 1295                    ["documentName"] = new Dictionary<string, object?>
 1296                    {
 1297                        ["type"] = "string",
 1298                        ["description"] = "Name of the context document referenced"
 1299                    },
 1300                    ["details"] = new Dictionary<string, object?>
 1301                    {
 1302                        ["type"] = "string",
 1303                        ["description"] = "Brief summary explaining why the document or parts of it offered limited insi
 1304                    }
 1305                },
 1306                ["required"] = new[] { "documentName", "details" },
 1307                ["additionalProperties"] = false
 1308            };
 309
 1310            var contextSources = new Dictionary<string, object?>
 1311            {
 1312                ["type"] = "object",
 1313                ["properties"] = new Dictionary<string, object?>
 1314                {
 1315                    ["mostValuable"] = new Dictionary<string, object?>
 1316                    {
 1317                        ["type"] = "array",
 1318                        ["items"] = mostValuableContextSourceItem,
 1319                        ["description"] = "Context documents that most influenced the prediction",
 1320                        ["minItems"] = 0
 1321                    },
 1322                    ["leastValuable"] = new Dictionary<string, object?>
 1323                    {
 1324                        ["type"] = "array",
 1325                        ["items"] = leastValuableContextSourceItem,
 1326                        ["description"] = "Context documents that provided limited or no valuable insight",
 1327                        ["minItems"] = 0
 1328                    }
 1329                },
 1330                ["required"] = new[] { "leastValuable", "mostValuable" },
 1331                ["additionalProperties"] = false
 1332            };
 333
 1334            properties["justification"] = new Dictionary<string, object?>
 1335            {
 1336                ["type"] = "object",
 1337                ["properties"] = new Dictionary<string, object?>
 1338                {
 1339                    ["keyReasoning"] = new Dictionary<string, object?>
 1340                    {
 1341                        ["type"] = "string",
 1342                        ["description"] = "Concise analytic summary motivating the predicted scoreline"
 1343                    },
 1344                    ["contextSources"] = contextSources,
 1345                    ["uncertainties"] = new Dictionary<string, object?>
 1346                    {
 1347                        ["type"] = "array",
 1348                        ["items"] = new Dictionary<string, object?>
 1349                        {
 1350                            ["type"] = "string",
 1351                            ["description"] = "Single uncertainty or external factor affecting confidence"
 1352                        },
 1353                        ["description"] = "Factors that could alter the predicted outcome",
 1354                        ["minItems"] = 0
 1355                    }
 1356                },
 1357                ["required"] = new[] { "contextSources", "keyReasoning", "uncertainties" },
 1358                ["additionalProperties"] = false
 1359            };
 1360            required.Add("justification");
 361        }
 362
 1363        var schema = new Dictionary<string, object?>
 1364        {
 1365            ["type"] = "object",
 1366            ["properties"] = properties,
 1367            ["required"] = required,
 1368            ["additionalProperties"] = false
 1369        };
 370
 1371        return JsonSerializer.SerializeToUtf8Bytes(schema);
 372    }
 373
 374    private Prediction ParsePrediction(string predictionJson)
 375    {
 376        try
 377        {
 1378            _logger.LogDebug("Parsing prediction JSON: {PredictionJson}", predictionJson);
 379
 1380            var predictionResponse = JsonSerializer.Deserialize<PredictionResponse>(predictionJson);
 1381            if (predictionResponse == null)
 382            {
 0383                LogRawModelResponse(predictionJson);
 0384                throw new InvalidOperationException("Failed to deserialize prediction response");
 385            }
 386
 1387            _logger.LogDebug("Parsed prediction response - Home: {Home}, Away: {Away}", predictionResponse.Home, predict
 388
 1389            PredictionJustification? justification = null;
 390
 1391            if (predictionResponse.Justification != null)
 392            {
 1393                var justificationResponse = predictionResponse.Justification;
 394
 1395                var mostValuable = justificationResponse.ContextSources?.MostValuable?
 1396                    .Where(entry => entry != null)
 1397                    .Select(entry => new PredictionJustificationContextSource(
 1398                        entry!.DocumentName?.Trim() ?? string.Empty,
 1399                        entry.Details?.Trim() ?? string.Empty))
 1400                    .ToList() ?? new List<PredictionJustificationContextSource>();
 401
 1402                var leastValuable = justificationResponse.ContextSources?.LeastValuable?
 0403                    .Where(entry => entry != null)
 0404                    .Select(entry => new PredictionJustificationContextSource(
 0405                        entry!.DocumentName?.Trim() ?? string.Empty,
 0406                        entry.Details?.Trim() ?? string.Empty))
 1407                    .ToList() ?? new List<PredictionJustificationContextSource>();
 408
 1409                var uncertainties = justificationResponse.Uncertainties?
 1410                    .Where(item => !string.IsNullOrWhiteSpace(item))
 1411                    .Select(item => item.Trim())
 1412                    .ToList() ?? new List<string>();
 413
 1414                justification = new PredictionJustification(
 1415                    justificationResponse.KeyReasoning?.Trim() ?? string.Empty,
 1416                    new PredictionJustificationContextSources(mostValuable, leastValuable),
 1417                    uncertainties);
 418
 1419                _logger.LogDebug(
 1420                    "Parsed justification with key reasoning: {KeyReasoning}; Most valuable sources: {MostValuableCount}
 1421                    justification.KeyReasoning,
 1422                    justification.ContextSources.MostValuable.Count,
 1423                    justification.ContextSources.LeastValuable.Count,
 1424                    justification.Uncertainties.Count);
 425            }
 426
 1427            return new Prediction(predictionResponse.Home, predictionResponse.Away, justification);
 428        }
 1429        catch (JsonException ex)
 430        {
 1431            _logger.LogError(ex, "Failed to parse prediction JSON: {PredictionJson}", predictionJson);
 1432            LogRawModelResponse(predictionJson);
 1433            throw new InvalidOperationException($"Failed to parse prediction response: {ex.Message}", ex);
 434        }
 1435    }
 436
 437    private void LogRawModelResponse(string rawResponse)
 438    {
 1439        if (string.IsNullOrWhiteSpace(rawResponse))
 440        {
 441            const string message = "Raw model response from OpenAI was empty or whitespace.";
 0442            _logger.LogError(message);
 0443            Console.Error.WriteLine(message);
 0444            return;
 445        }
 446
 1447        _logger.LogError("Raw model response from OpenAI: {RawResponse}", rawResponse);
 1448        Console.Error.WriteLine("Raw model response from OpenAI:");
 1449        Console.Error.WriteLine(rawResponse);
 1450    }
 451
 452    private string BuildBonusInstructions(IEnumerable<DocumentContext> contextDocuments)
 453    {
 454        // Use the pre-loaded bonus instructions template
 1455        var bonusInstructionsTemplate = _bonusInstructionsTemplate;
 456
 1457        var contextList = contextDocuments.ToList();
 1458        if (contextList.Any())
 459        {
 1460            var contextSection = "\n";
 1461            foreach (var doc in contextList)
 462            {
 1463                contextSection += "---\n";
 1464                contextSection += $"{doc.Name}\n\n";
 1465                contextSection += $"{doc.Content}\n";
 466            }
 1467            contextSection += "---";
 468
 1469            bonusInstructionsTemplate += contextSection;
 470
 1471            _logger.LogDebug("Added {ContextCount} context documents to bonus instructions", contextList.Count);
 472        }
 473        else
 474        {
 1475            _logger.LogDebug("No context documents provided for bonus predictions");
 476        }
 477
 1478        return bonusInstructionsTemplate;
 479    }
 480
 481    private static string CreateBonusQuestionJson(BonusQuestion question)
 482    {
 1483        var questionData = new
 1484        {
 1485            text = question.Text,
 1486            options = question.Options.Select(o => new { id = o.Id, text = o.Text }).ToArray(),
 1487            maxSelections = question.MaxSelections
 1488        };
 489
 1490        return JsonSerializer.Serialize(questionData, new JsonSerializerOptions
 1491        {
 1492            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
 1493        });
 494    }
 495
 496    private static byte[] CreateSingleBonusPredictionJsonSchema(BonusQuestion question)
 497    {
 498        // For multi-selection questions, require exactly MaxSelections answers
 499        // For single-selection questions, require exactly 1 answer
 1500        var requiredSelections = question.MaxSelections;
 501
 1502        var schema = new
 1503        {
 1504            type = "object",
 1505            properties = new
 1506            {
 1507                selectedOptionIds = new
 1508                {
 1509                    type = "array",
 1510                    items = new { type = "string", @enum = question.Options.Select(o => o.Id).ToArray() },
 1511                    minItems = requiredSelections,
 1512                    maxItems = requiredSelections
 1513                }
 1514            },
 1515            required = new[] { "selectedOptionIds" },
 1516            additionalProperties = false
 1517        };
 518
 1519        return JsonSerializer.SerializeToUtf8Bytes(schema);
 520    }
 521
 522    private BonusPrediction? ParseSingleBonusPrediction(string predictionJson, BonusQuestion question)
 523    {
 524        try
 525        {
 1526            _logger.LogDebug("Parsing single bonus prediction JSON: {PredictionJson}", predictionJson);
 527
 1528            var response = JsonSerializer.Deserialize<SingleBonusPredictionResponse>(predictionJson);
 1529            if (response?.SelectedOptionIds?.Any() != true)
 530            {
 1531                throw new InvalidOperationException("Failed to deserialize bonus prediction response or no options selec
 532            }
 533
 534            // Validate that all selected options exist for this question
 1535            var validOptionIds = question.Options.Select(o => o.Id).ToHashSet();
 1536            var invalidOptions = response.SelectedOptionIds.Where(id => !validOptionIds.Contains(id)).ToArray();
 537
 1538            if (invalidOptions.Any())
 539            {
 1540                _logger.LogWarning("Invalid option IDs for question '{QuestionText}': {InvalidOptions}",
 1541                    question.Text, string.Join(", ", invalidOptions));
 1542                return null;
 543            }
 544
 545            // Validate no duplicate selections
 1546            var duplicateOptions = response.SelectedOptionIds
 1547                .GroupBy(id => id)
 1548                .Where(g => g.Count() > 1)
 1549                .Select(g => g.Key)
 1550                .ToArray();
 551
 1552            if (duplicateOptions.Any())
 553            {
 1554                _logger.LogWarning("Duplicate option IDs for question '{QuestionText}': {DuplicateOptions}",
 1555                    question.Text, string.Join(", ", duplicateOptions));
 1556                return null;
 557            }
 558
 559            // Validate selection count - must match exactly MaxSelections for full predictions
 1560            if (response.SelectedOptionIds.Length != question.MaxSelections)
 561            {
 1562                _logger.LogWarning("Invalid selection count for question '{QuestionText}': expected exactly {MaxSelectio
 1563                    question.Text, question.MaxSelections, response.SelectedOptionIds.Length);
 1564                return null;
 565            }
 566
 1567            var prediction = new BonusPrediction(response.SelectedOptionIds.ToList());
 568
 1569            _logger.LogDebug("Parsed prediction: {SelectedOptions}",
 1570                string.Join(", ", response.SelectedOptionIds));
 571
 1572            return prediction;
 573        }
 1574        catch (JsonException ex)
 575        {
 1576            _logger.LogError(ex, "Failed to parse bonus prediction JSON: {PredictionJson}", predictionJson);
 1577            return null;
 578        }
 1579    }
 580
 581    /// <summary>
 582    /// Gets the file path of the match prediction prompt being used by this service
 583    /// </summary>
 584    /// <returns>The absolute file path to the match prompt file</returns>
 1585    public string GetMatchPromptPath(bool includeJustification = false) => includeJustification ? _matchPromptPathWithJu
 586
 587    /// <summary>
 588    /// Gets the file path of the bonus question prediction prompt being used by this service
 589    /// </summary>
 590    /// <returns>The absolute file path to the bonus prompt file</returns>
 1591    public string GetBonusPromptPath() => _bonusPromptPath;
 592
 593    /// <summary>
 594    /// Internal class for deserializing the structured prediction response
 595    /// </summary>
 596    private class PredictionResponse
 597    {
 598        [JsonPropertyName("home")]
 1599        public int Home { get; set; }
 600
 601        [JsonPropertyName("away")]
 1602        public int Away { get; set; }
 603
 604        [JsonPropertyName("justification")]
 1605        public JustificationResponse? Justification { get; set; }
 606    }
 607
 608    private class JustificationResponse
 609    {
 610        [JsonPropertyName("keyReasoning")]
 1611        public string KeyReasoning { get; set; } = string.Empty;
 612
 613        [JsonPropertyName("contextSources")]
 1614        public JustificationContextSourcesResponse ContextSources { get; set; } = new();
 615
 616        [JsonPropertyName("uncertainties")]
 1617        public string[] Uncertainties { get; set; } = Array.Empty<string>();
 618    }
 619
 620    private class JustificationContextSourcesResponse
 621    {
 622        [JsonPropertyName("mostValuable")]
 1623        public JustificationContextSourceEntry[] MostValuable { get; set; } = Array.Empty<JustificationContextSourceEntr
 624
 625        [JsonPropertyName("leastValuable")]
 1626        public JustificationContextSourceEntry[] LeastValuable { get; set; } = Array.Empty<JustificationContextSourceEnt
 627    }
 628
 629    private class JustificationContextSourceEntry
 630    {
 631        [JsonPropertyName("documentName")]
 1632        public string DocumentName { get; set; } = string.Empty;
 633
 634        [JsonPropertyName("details")]
 1635        public string Details { get; set; } = string.Empty;
 636    }
 637
 638    /// <summary>
 639    /// Internal class for deserializing the bonus predictions response
 640    /// </summary>
 641    private class BonusPredictionsResponse
 642    {
 643        [JsonPropertyName("predictions")]
 0644        public BonusPredictionEntry[]? Predictions { get; set; }
 645    }
 646
 647    /// <summary>
 648    /// Internal class for deserializing individual bonus prediction entries
 649    /// </summary>
 650    private class BonusPredictionEntry
 651    {
 652        [JsonPropertyName("questionId")]
 0653        public string QuestionId { get; set; } = string.Empty;
 654
 655        [JsonPropertyName("selectedOptionIds")]
 0656        public string[] SelectedOptionIds { get; set; } = Array.Empty<string>();
 657    }
 658
 659    /// <summary>
 660    /// Internal class for deserializing single bonus prediction response
 661    /// </summary>
 662    private class SingleBonusPredictionResponse
 663    {
 664        [JsonPropertyName("selectedOptionIds")]
 1665        public string[] SelectedOptionIds { get; set; } = Array.Empty<string>();
 666    }
 667}

Methods/Properties

.ctor(OpenAI.Chat.ChatClient, Microsoft.Extensions.Logging.ILogger<OpenAiIntegration.PredictionService>, OpenAiIntegration.ICostCalculationService, OpenAiIntegration.ITokenUsageTracker, OpenAiIntegration.IInstructionsTemplateProvider, string)
PredictMatchAsync()
PredictBonusQuestionAsync()
BuildInstructions(System.Collections.Generic.IEnumerable<EHonda.KicktippAi.Core.DocumentContext>, bool)
CreateMatchJson(EHonda.KicktippAi.Core.Match)
BuildPredictionJsonSchema(bool)
ParsePrediction(string)
LogRawModelResponse(string)
BuildBonusInstructions(System.Collections.Generic.IEnumerable<EHonda.KicktippAi.Core.DocumentContext>)
CreateBonusQuestionJson(EHonda.KicktippAi.Core.BonusQuestion)
CreateSingleBonusPredictionJsonSchema(EHonda.KicktippAi.Core.BonusQuestion)
ParseSingleBonusPrediction(string, EHonda.KicktippAi.Core.BonusQuestion)
GetMatchPromptPath(bool)
GetBonusPromptPath()
get_Home()
set_Home(int)
get_Away()
set_Away(int)
get_Justification()
set_Justification(OpenAiIntegration.PredictionService.JustificationResponse)
get_KeyReasoning()
set_KeyReasoning(string)
.ctor()
get_ContextSources()
set_ContextSources(OpenAiIntegration.PredictionService.JustificationContextSourcesResponse)
get_Uncertainties()
set_Uncertainties(string[])
get_MostValuable()
set_MostValuable(OpenAiIntegration.PredictionService.JustificationContextSourceEntry[])
.ctor()
get_LeastValuable()
set_LeastValuable(OpenAiIntegration.PredictionService.JustificationContextSourceEntry[])
get_DocumentName()
set_DocumentName(string)
.ctor()
get_Details()
set_Details(string)
get_Predictions()
set_Predictions(OpenAiIntegration.PredictionService.BonusPredictionEntry[])
get_QuestionId()
set_QuestionId(string)
.ctor()
get_SelectedOptionIds()
set_SelectedOptionIds(string[])
get_SelectedOptionIds()
set_SelectedOptionIds(string[])
.ctor()