| | | 1 | | using System.ClientModel; |
| | | 2 | | using EHonda.KicktippAi.Core; |
| | | 3 | | using Microsoft.Extensions.Logging; |
| | | 4 | | using OpenAI.Responses; |
| | | 5 | | |
| | | 6 | | namespace OpenAiIntegration; |
| | | 7 | | |
| | | 8 | | public class OpenAiPredictor : IPredictor<PredictorContext> |
| | | 9 | | { |
| | | 10 | | private readonly ResponsesClient _client; |
| | | 11 | | private readonly ILogger<OpenAiPredictor> _logger; |
| | | 12 | | private readonly string _model; |
| | | 13 | | |
| | 1 | 14 | | public OpenAiPredictor(ResponsesClient client, ILogger<OpenAiPredictor> logger, string model = "gpt-4o-mini") |
| | | 15 | | { |
| | 1 | 16 | | _client = client ?? throw new ArgumentNullException(nameof(client)); |
| | 1 | 17 | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | 1 | 18 | | _model = model ?? throw new ArgumentNullException(nameof(model)); |
| | 1 | 19 | | } |
| | | 20 | | |
| | | 21 | | public async Task<Prediction> PredictAsync(Match match, PredictorContext context, CancellationToken cancellationToke |
| | | 22 | | { |
| | 1 | 23 | | _logger.LogInformation("Generating prediction for match: {HomeTeam} vs {AwayTeam} at {StartTime}", |
| | 1 | 24 | | match.HomeTeam, match.AwayTeam, match.StartsAt); |
| | | 25 | | |
| | | 26 | | try |
| | | 27 | | { |
| | 1 | 28 | | var prompt = GeneratePrompt(match, context); |
| | 1 | 29 | | _logger.LogDebug("Generated prompt: {Prompt}", prompt); |
| | | 30 | | |
| | 1 | 31 | | var response = await _client.CreateResponseAsync(_model, prompt, cancellationToken: cancellationToken); |
| | 1 | 32 | | _logger.LogDebug("Received response from OpenAI"); |
| | | 33 | | |
| | 1 | 34 | | var prediction = ParsePrediction(response); |
| | 1 | 35 | | _logger.LogInformation("Prediction generated: {HomeGoals}-{AwayGoals} for {HomeTeam} vs {AwayTeam}", |
| | 1 | 36 | | prediction.HomeGoals, prediction.AwayGoals, match.HomeTeam, match.AwayTeam); |
| | | 37 | | |
| | 1 | 38 | | return prediction; |
| | | 39 | | } |
| | 1 | 40 | | catch (Exception ex) |
| | | 41 | | { |
| | 1 | 42 | | _logger.LogError(ex, "Error generating prediction for match: {HomeTeam} vs {AwayTeam}", |
| | 1 | 43 | | match.HomeTeam, match.AwayTeam); |
| | | 44 | | |
| | | 45 | | // Return a fallback prediction in case of error |
| | 1 | 46 | | _logger.LogWarning("Returning fallback prediction (1-1) due to error"); |
| | 1 | 47 | | return new Prediction(1, 1); |
| | | 48 | | } |
| | 1 | 49 | | } |
| | | 50 | | |
| | | 51 | | private string GeneratePrompt(Match match, PredictorContext context) |
| | | 52 | | { |
| | 1 | 53 | | var prompt = $@"You are a football prediction expert. Predict the final score for this match: |
| | 1 | 54 | | |
| | 1 | 55 | | Match: {match.HomeTeam} vs {match.AwayTeam} |
| | 1 | 56 | | Kick-off: {match.StartsAt:yyyy-MM-dd HH:mm} |
| | 1 | 57 | | |
| | 1 | 58 | | Please provide your prediction in the following format only: |
| | 1 | 59 | | HOME_GOALS-AWAY_GOALS |
| | 1 | 60 | | |
| | 1 | 61 | | For example: 2-1 |
| | 1 | 62 | | |
| | 1 | 63 | | Consider: |
| | 1 | 64 | | - Home advantage (home teams typically score slightly more) |
| | 1 | 65 | | - Recent form and performance |
| | 1 | 66 | | - Common football scores (0-0, 1-0, 1-1, 2-0, 2-1, etc.) |
| | 1 | 67 | | |
| | 1 | 68 | | Your prediction:"; |
| | | 69 | | |
| | 1 | 70 | | return prompt; |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | private Prediction ParsePrediction(ClientResult<ResponseResult>? response) |
| | | 74 | | { |
| | | 75 | | try |
| | | 76 | | { |
| | 1 | 77 | | if (response?.Value is null) |
| | | 78 | | { |
| | 0 | 79 | | _logger.LogWarning("No content in OpenAI response, using fallback prediction"); |
| | 0 | 80 | | return new Prediction(1, 1); |
| | | 81 | | } |
| | | 82 | | |
| | 1 | 83 | | var content = response.Value.GetOutputText()?.Trim(); |
| | 1 | 84 | | if (string.IsNullOrEmpty(content)) |
| | | 85 | | { |
| | 1 | 86 | | _logger.LogWarning("Empty content in OpenAI response, using fallback prediction"); |
| | 1 | 87 | | return new Prediction(1, 1); |
| | | 88 | | } |
| | | 89 | | |
| | 1 | 90 | | _logger.LogDebug("Parsing response content: {Content}", content); |
| | | 91 | | |
| | | 92 | | // Look for pattern like "2-1" in the response |
| | 1 | 93 | | var scorePattern = System.Text.RegularExpressions.Regex.Match(content, @"(\d+)-(\d+)"); |
| | | 94 | | |
| | 1 | 95 | | if (scorePattern.Success) |
| | | 96 | | { |
| | 1 | 97 | | var homeGoals = int.Parse(scorePattern.Groups[1].Value); |
| | 1 | 98 | | var awayGoals = int.Parse(scorePattern.Groups[2].Value); |
| | | 99 | | |
| | | 100 | | // Validate reasonable score range (0-10 goals per team) |
| | 1 | 101 | | if (homeGoals >= 0 && homeGoals <= 10 && awayGoals >= 0 && awayGoals <= 10) |
| | | 102 | | { |
| | 1 | 103 | | return new Prediction(homeGoals, awayGoals); |
| | | 104 | | } |
| | | 105 | | else |
| | | 106 | | { |
| | 1 | 107 | | _logger.LogWarning("Parsed scores out of reasonable range: {HomeGoals}-{AwayGoals}, using fallback", |
| | 1 | 108 | | homeGoals, awayGoals); |
| | 1 | 109 | | return new Prediction(1, 1); |
| | | 110 | | } |
| | | 111 | | } |
| | | 112 | | else |
| | | 113 | | { |
| | 1 | 114 | | _logger.LogWarning("Could not parse score from response: {Content}, using fallback prediction", content) |
| | 1 | 115 | | return new Prediction(1, 1); |
| | | 116 | | } |
| | | 117 | | } |
| | 0 | 118 | | catch (Exception ex) |
| | | 119 | | { |
| | 0 | 120 | | _logger.LogError(ex, "Error parsing prediction response, using fallback prediction"); |
| | 0 | 121 | | return new Prediction(1, 1); |
| | | 122 | | } |
| | 1 | 123 | | } |
| | | 124 | | } |