< Summary

Information
Class: Orchestrator.Commands.Operations.Bonus.BonusCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/Bonus/BonusCommand.cs
Line coverage
100%
Covered lines: 180
Uncovered lines: 0
Coverable lines: 180
Total lines: 391
Line coverage: 100%
Branch coverage
94%
Covered branches: 110
Total branches: 116
Branch coverage: 94.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()100%2626100%
ExecuteBonusWorkflow()93.33%9090100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/Bonus/BonusCommand.cs

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2using Microsoft.Extensions.Logging;
 3using Spectre.Console.Cli;
 4using Spectre.Console;
 5using OpenAiIntegration;
 6using Orchestrator.Commands.Operations.Matchday;
 7using Orchestrator.Commands.Shared;
 8using Orchestrator.Infrastructure.Factories;
 9
 10namespace Orchestrator.Commands.Operations.Bonus;
 11
 12public class BonusCommand : AsyncCommand<BaseSettings>
 13{
 14    private readonly IAnsiConsole _console;
 15    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 16    private readonly IKicktippClientFactory _kicktippClientFactory;
 17    private readonly IOpenAiServiceFactory _openAiServiceFactory;
 18    private readonly IContextProviderFactory _contextProviderFactory;
 19    private readonly ILogger<BonusCommand> _logger;
 20
 121    public BonusCommand(
 122        IAnsiConsole console,
 123        IFirebaseServiceFactory firebaseServiceFactory,
 124        IKicktippClientFactory kicktippClientFactory,
 125        IOpenAiServiceFactory openAiServiceFactory,
 126        IContextProviderFactory contextProviderFactory,
 127        ILogger<BonusCommand> logger)
 28    {
 129        _console = console;
 130        _firebaseServiceFactory = firebaseServiceFactory;
 131        _kicktippClientFactory = kicktippClientFactory;
 132        _openAiServiceFactory = openAiServiceFactory;
 133        _contextProviderFactory = contextProviderFactory;
 134        _logger = logger;
 135    }
 36
 37    public override async Task<int> ExecuteAsync(CommandContext context, BaseSettings settings)
 38    {
 39
 40        try
 41        {
 142            _console.MarkupLine($"[green]Bonus command initialized with model:[/] [yellow]{settings.Model}[/]");
 43
 144            if (settings.Verbose)
 45            {
 146                _console.MarkupLine("[dim]Verbose mode enabled[/]");
 47            }
 48
 149            if (settings.OverrideKicktipp)
 50            {
 151                _console.MarkupLine("[yellow]Override mode enabled - will override existing Kicktipp predictions[/]");
 52            }
 53
 154            if (settings.OverrideDatabase)
 55            {
 156                _console.MarkupLine("[yellow]Override database mode enabled - will override existing database prediction
 57            }
 58
 159            if (settings.Agent)
 60            {
 161                _console.MarkupLine("[blue]Agent mode enabled - prediction details will be hidden[/]");
 62            }
 63
 164            if (settings.DryRun)
 65            {
 166                _console.MarkupLine("[magenta]Dry run mode enabled - no changes will be made to database or Kicktipp[/]"
 67            }
 68
 169            if (!string.IsNullOrEmpty(settings.EstimatedCostsModel))
 70            {
 171                _console.MarkupLine($"[cyan]Estimated costs will be calculated for model:[/] [yellow]{settings.Estimated
 72            }
 73
 74            // Validate reprediction settings
 175            if (settings.OverrideDatabase && settings.IsRepredictMode)
 76            {
 177                _console.MarkupLine($"[red]Error:[/] --override-database cannot be used with reprediction flags (--repre
 178                return 1;
 79            }
 80
 181            if (settings.MaxRepredictions.HasValue && settings.MaxRepredictions.Value < 0)
 82            {
 183                _console.MarkupLine($"[red]Error:[/] --max-repredictions must be 0 or greater");
 184                return 1;
 85            }
 86
 187            if (settings.IsRepredictMode)
 88            {
 189                var maxValue = settings.MaxRepredictions ?? int.MaxValue;
 190                _console.MarkupLine($"[yellow]Reprediction mode enabled - max repredictions: {(settings.MaxRepredictions
 91            }
 92
 93            // Execute the bonus prediction workflow
 194            await ExecuteBonusWorkflow(settings);
 95
 196            return 0;
 97        }
 198        catch (Exception ex)
 99        {
 1100            _logger.LogError(ex, "Error executing bonus command");
 1101            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 1102            return 1;
 103        }
 1104    }
 105
 106    private async Task ExecuteBonusWorkflow(BaseSettings settings)
 107    {
 108        // Create services using factories
 1109        var kicktippClient = _kicktippClientFactory.CreateClient();
 1110        var predictionService = _openAiServiceFactory.CreatePredictionService(settings.Model);
 111
 112        // Log the prompt paths being used
 1113        if (settings.Verbose)
 114        {
 1115            _console.MarkupLine($"[dim]Bonus prompt:[/] [blue]{predictionService.GetBonusPromptPath()}[/]");
 116        }
 117
 118        // Create KPI Context Provider for bonus predictions using factory
 1119        var kpiContextProvider = _contextProviderFactory.CreateKpiContextProvider();
 120
 1121        var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 122
 123        // Create prediction repository
 1124        var predictionRepository = _firebaseServiceFactory.CreatePredictionRepository();
 1125        var databaseEnabled = true;
 126
 127        // Reset token usage tracker for this workflow
 1128        tokenUsageTracker.Reset();
 129
 130        // Determine community context (use explicit setting or fall back to community name)
 1131        string communityContext = settings.CommunityContext ?? settings.Community;
 132
 1133        _console.MarkupLine($"[blue]Using community:[/] [yellow]{settings.Community}[/]");
 1134        _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 1135        _console.MarkupLine("[blue]Getting open bonus questions from Kicktipp...[/]");
 136
 137        // Step 1: Get open bonus questions from Kicktipp
 1138        var bonusQuestions = await kicktippClient.GetOpenBonusQuestionsAsync(settings.Community);
 139
 1140        if (!bonusQuestions.Any())
 141        {
 1142            _console.MarkupLine("[yellow]No open bonus questions found[/]");
 1143            return;
 144        }
 145
 1146        _console.MarkupLine($"[green]Found {bonusQuestions.Count} open bonus questions[/]");
 147
 1148        if (databaseEnabled)
 149        {
 1150            _console.MarkupLine("[blue]Database enabled - checking for existing predictions...[/]");
 151        }
 152
 1153        var predictions = new Dictionary<string, BonusPrediction>();
 154
 155        // Step 2: For each question, check database first, then predict if needed
 1156        foreach (var question in bonusQuestions)
 157        {
 1158            _console.MarkupLine($"[cyan]Processing:[/] {Markup.Escape(question.Text)}");
 159
 160            try
 161            {
 1162                BonusPrediction? prediction = null;
 1163                bool fromDatabase = false;
 1164                bool shouldPredict = false;
 165
 166                // Check if we have an existing prediction in the database
 1167                if (databaseEnabled && !settings.OverrideDatabase && !settings.IsRepredictMode)
 168                {
 169                    // Look for prediction by question text, model, and community context
 1170                    prediction = await predictionRepository!.GetBonusPredictionByTextAsync(question.Text, settings.Model
 1171                    if (prediction != null)
 172                    {
 1173                        fromDatabase = true;
 1174                        if (settings.Agent)
 175                        {
 1176                            _console.MarkupLine($"[green]  ✓ Found existing prediction[/] [dim](from database)[/]");
 177                        }
 178                        else
 179                        {
 1180                            var optionTexts = question.Options
 1181                                .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1182                                .Select(o => o.Text);
 1183                            _console.MarkupLine($"[green]  ✓ Found existing prediction:[/] {string.Join(", ", optionText
 184                        }
 185                    }
 186                }
 187
 188                // Handle reprediction logic
 1189                if (settings.IsRepredictMode && databaseEnabled)
 190                {
 1191                    var currentRepredictionIndex = await predictionRepository!.GetBonusRepredictionIndexAsync(question.T
 192
 1193                    if (currentRepredictionIndex == -1)
 194                    {
 195                        // No prediction exists yet - create first prediction
 1196                        shouldPredict = true;
 1197                        _console.MarkupLine($"[yellow]  → No existing prediction found, creating first prediction...[/]"
 198                    }
 199                    else
 200                    {
 201                        // Check if we can create another reprediction
 1202                        var maxAllowed = settings.MaxRepredictions ?? int.MaxValue;
 1203                        var nextIndex = currentRepredictionIndex + 1;
 204
 1205                        if (nextIndex <= maxAllowed)
 206                        {
 1207                            shouldPredict = true;
 1208                            _console.MarkupLine($"[yellow]  → Creating reprediction {nextIndex} (current: {currentRepred
 209                        }
 210                        else
 211                        {
 1212                            _console.MarkupLine($"[yellow]  ✗ Skipped - already at max repredictions ({currentRepredicti
 213
 214                            // Get the latest prediction for display purposes
 1215                            prediction = await predictionRepository!.GetBonusPredictionByTextAsync(question.Text, settin
 1216                            if (prediction != null)
 217                            {
 1218                                fromDatabase = true;
 1219                                if (!settings.Agent)
 220                                {
 1221                                    var optionTexts = question.Options
 1222                                        .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1223                                        .Select(o => o.Text);
 1224                                    _console.MarkupLine($"[green]  ✓ Latest prediction:[/] {string.Join(", ", optionText
 225                                }
 226                            }
 227                        }
 228                    }
 229                }
 230
 231                // If no existing prediction (normal mode) or we need to predict (reprediction mode), generate a new one
 1232                if (prediction == null || shouldPredict)
 233                {
 1234                    _console.MarkupLine($"[yellow]  → Generating new prediction...[/]");
 235
 236                    // Step 3: Get KPI context for bonus predictions
 1237                    var contextDocuments = new List<DocumentContext>();
 238
 239                    // Use KPI documents as context for bonus predictions (targeted by question content)
 1240                    await foreach (var context in kpiContextProvider.GetBonusQuestionContextAsync(question.Text, communi
 241                    {
 1242                        contextDocuments.Add(context);
 243                    }
 244
 1245                    if (settings.Verbose)
 246                    {
 1247                        _console.MarkupLine($"[dim]    Using {contextDocuments.Count} KPI context documents[/]");
 248                    }
 249
 250                    // Predict the bonus question
 1251                    prediction = await predictionService.PredictBonusQuestionAsync(question, contextDocuments);
 252
 1253                    if (prediction != null)
 254                    {
 1255                        if (settings.Agent)
 256                        {
 1257                            _console.MarkupLine($"[green]  ✓ Generated prediction[/]");
 258                        }
 259                        else
 260                        {
 1261                            var optionTexts = question.Options
 1262                                .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1263                                .Select(o => o.Text);
 1264                            _console.MarkupLine($"[green]  ✓ Generated prediction:[/] {string.Join(", ", optionTexts)}")
 265                        }
 266
 267                        // Save to database immediately if enabled
 1268                        if (databaseEnabled && !settings.DryRun)
 269                        {
 270                            try
 271                            {
 272                                // Get token usage and cost information
 1273                                var cost = (double)tokenUsageTracker.GetLastCost(); // Get the cost for this individual 
 274                                // Use the new GetLastUsageJson method to get full JSON
 1275                                var tokenUsageJson = tokenUsageTracker.GetLastUsageJson() ?? "{}";
 276
 1277                                if (settings.IsRepredictMode)
 278                                {
 279                                    // Save as reprediction with specific index
 1280                                    var currentIndex = await predictionRepository!.GetBonusRepredictionIndexAsync(questi
 1281                                    var nextIndex = currentIndex == -1 ? 0 : currentIndex + 1;
 282
 1283                                    await predictionRepository!.SaveBonusRepredictionAsync(
 1284                                        question,
 1285                                        prediction,
 1286                                        settings.Model,
 1287                                        tokenUsageJson,
 1288                                        cost,
 1289                                        communityContext,
 1290                                        contextDocuments.Select(d => d.Name),
 1291                                        nextIndex);
 292
 1293                                    if (settings.Verbose)
 294                                    {
 1295                                        _console.MarkupLine($"[dim]    ✓ Saved as reprediction {nextIndex} to database[/
 296                                    }
 297                                }
 298                                else
 299                                {
 300                                    // Save normally (override or new prediction)
 1301                                    await predictionRepository!.SaveBonusPredictionAsync(
 1302                                        question,
 1303                                        prediction,
 1304                                        settings.Model,
 1305                                        tokenUsageJson,
 1306                                        cost,
 1307                                        communityContext,
 1308                                        contextDocuments.Select(d => d.Name),
 1309                                        overrideCreatedAt: settings.OverrideDatabase);
 310
 1311                                    if (settings.Verbose)
 312                                    {
 1313                                        _console.MarkupLine($"[dim]    ✓ Saved to database[/]");
 314                                    }
 315                                }
 1316                            }
 1317                            catch (Exception ex)
 318                            {
 1319                                _logger.LogError(ex, "Failed to save bonus prediction for question '{QuestionText}'", qu
 1320                                _console.MarkupLine($"[red]    ✗ Failed to save to database: {ex.Message}[/]");
 1321                            }
 322                        }
 1323                        else if (databaseEnabled && settings.DryRun && settings.Verbose)
 324                        {
 1325                            _console.MarkupLine($"[dim]    (Dry run - skipped database save)[/]");
 326                        }
 327
 328                        // Show individual question token usage in verbose mode
 1329                        if (settings.Verbose)
 330                        {
 1331                            var questionUsage = !string.IsNullOrEmpty(settings.EstimatedCostsModel)
 1332                                ? tokenUsageTracker.GetLastUsageCompactSummaryWithEstimatedCosts(settings.EstimatedCosts
 1333                                : tokenUsageTracker.GetLastUsageCompactSummary();
 1334                            _console.MarkupLine($"[dim]    Token usage: {questionUsage}[/]");
 335                        }
 336                    }
 337                    else
 338                    {
 1339                        _console.MarkupLine($"[red]  ✗ Failed to generate prediction[/]");
 1340                        continue;
 341                    }
 1342                }
 343
 1344                predictions[question.FormFieldName ?? question.Text] = prediction;
 345
 1346                if (!fromDatabase && settings.Verbose)
 347                {
 1348                    _console.MarkupLine($"[dim]    Ready for Kicktipp placement[/]");
 349                }
 1350            }
 1351            catch (Exception ex)
 352            {
 1353                _logger.LogError(ex, "Error processing bonus question '{QuestionText}'", question.Text);
 1354                _console.MarkupLine($"[red]  ✗ Error processing question: {ex.Message}[/]");
 1355            }
 1356        }
 357
 1358        if (!predictions.Any())
 359        {
 1360            _console.MarkupLine("[yellow]No predictions available, nothing to place[/]");
 1361            return;
 362        }
 363
 364        // Step 4: Place all predictions using PlaceBonusPredictionsAsync
 1365        _console.MarkupLine($"[blue]Placing {predictions.Count} bonus predictions to Kicktipp...[/]");
 366
 1367        if (settings.DryRun)
 368        {
 1369            _console.MarkupLine($"[magenta]✓ Dry run mode - would have placed {predictions.Count} bonus predictions (no 
 370        }
 371        else
 372        {
 1373            var success = await kicktippClient.PlaceBonusPredictionsAsync(settings.Community, predictions, overridePredi
 374
 1375            if (success)
 376            {
 1377                _console.MarkupLine($"[green]✓ Successfully placed all {predictions.Count} bonus predictions![/]");
 378            }
 379            else
 380            {
 1381                _console.MarkupLine("[red]✗ Failed to place some or all bonus predictions[/]");
 382            }
 383        }
 384
 385        // Display token usage summary
 1386        var summary = !string.IsNullOrEmpty(settings.EstimatedCostsModel)
 1387            ? tokenUsageTracker.GetCompactSummaryWithEstimatedCosts(settings.EstimatedCostsModel)
 1388            : tokenUsageTracker.GetCompactSummary();
 1389        _console.MarkupLine($"[dim]Token usage (uncached/cached/reasoning/output/$cost): {summary}[/]");
 1390    }
 391}