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