< 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
97%
Covered lines: 295
Uncovered lines: 8
Coverable lines: 303
Total lines: 609
Line coverage: 97.3%
Branch coverage
90%
Covered branches: 155
Total branches: 172
Branch coverage: 90.1%
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%11100%
ExecuteWithSettingsAsync()96.43%2828100%
.cctor()100%11100%
ExecuteBonusWorkflow()92.86%126126100%
EnsureWorldCupRankingKpiPresentAsync()100%22100%
CheckBonusPredictionOutdated()56.25%251666.67%

File(s)

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

#LineLine coverage
 1using System.Text.Json;
 2using EHonda.KicktippAi.Core;
 3using Microsoft.Extensions.Logging;
 4using Spectre.Console.Cli;
 5using Spectre.Console;
 6using OpenAiIntegration;
 7using Orchestrator.Commands.Operations.Matchday;
 8using Orchestrator.Commands.Shared;
 9using Orchestrator.Infrastructure;
 10using Orchestrator.Infrastructure.Factories;
 11using Orchestrator.Infrastructure.Langfuse;
 12
 13namespace Orchestrator.Commands.Operations.Bonus;
 14
 15public class BonusCommand : AsyncCommand<BaseSettings>
 16{
 17    private const string FifaRankingsDocumentName = "fifa-rankings";
 18
 19    private readonly IAnsiConsole _console;
 20    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 21    private readonly IKicktippClientFactory _kicktippClientFactory;
 22    private readonly IOpenAiServiceFactory _openAiServiceFactory;
 23    private readonly IContextProviderFactory _contextProviderFactory;
 24    private readonly ILogger<BonusCommand> _logger;
 25    private readonly ILangfusePublicApiClient? _langfuseClient;
 26
 127    public BonusCommand(
 128        IAnsiConsole console,
 129        IFirebaseServiceFactory firebaseServiceFactory,
 130        IKicktippClientFactory kicktippClientFactory,
 131        IOpenAiServiceFactory openAiServiceFactory,
 132        IContextProviderFactory contextProviderFactory,
 133        ILogger<BonusCommand> logger,
 134        ILangfusePublicApiClient? langfuseClient = null)
 35    {
 136        _console = console;
 137        _firebaseServiceFactory = firebaseServiceFactory;
 138        _kicktippClientFactory = kicktippClientFactory;
 139        _openAiServiceFactory = openAiServiceFactory;
 140        _contextProviderFactory = contextProviderFactory;
 141        _logger = logger;
 142        _langfuseClient = langfuseClient;
 143    }
 44
 45    protected override async Task<int> ExecuteAsync(CommandContext context, BaseSettings settings, CancellationToken can
 46    {
 147        return await ExecuteWithSettingsAsync(settings, cancellationToken);
 148    }
 49
 50    internal async Task<int> ExecuteWithSettingsAsync(BaseSettings settings, CancellationToken cancellationToken = defau
 51    {
 52
 53        try
 54        {
 155            var initialModel = string.IsNullOrWhiteSpace(settings.Model) ? "(competition default)" : settings.Model;
 156            _console.MarkupLine($"[green]Bonus command initialized with model:[/] [yellow]{initialModel}[/]");
 57
 158            if (settings.Verbose)
 59            {
 160                _console.MarkupLine("[dim]Verbose mode enabled[/]");
 61            }
 62
 163            if (settings.OverrideKicktipp)
 64            {
 165                _console.MarkupLine("[yellow]Override mode enabled - will override existing Kicktipp predictions[/]");
 66            }
 67
 168            if (settings.OverrideDatabase)
 69            {
 170                _console.MarkupLine("[yellow]Override database mode enabled - will override existing database prediction
 71            }
 72
 173            if (settings.Agent)
 74            {
 175                _console.MarkupLine("[blue]Agent mode enabled - prediction details will be hidden[/]");
 76            }
 77
 178            if (settings.DryRun)
 79            {
 180                _console.MarkupLine("[magenta]Dry run mode enabled - no changes will be made to database or Kicktipp[/]"
 81            }
 82
 183            if (!string.IsNullOrEmpty(settings.EstimatedCostsModel))
 84            {
 185                _console.MarkupLine($"[cyan]Estimated costs will be calculated for model:[/] [yellow]{settings.Estimated
 86            }
 87
 88            // Validate reprediction settings
 189            if (settings.OverrideDatabase && settings.IsRepredictMode)
 90            {
 191                _console.MarkupLine($"[red]Error:[/] --override-database cannot be used with reprediction flags (--repre
 192                return 1;
 93            }
 94
 195            if (settings.MaxRepredictions.HasValue && settings.MaxRepredictions.Value < 0)
 96            {
 197                _console.MarkupLine($"[red]Error:[/] --max-repredictions must be 0 or greater");
 198                return 1;
 99            }
 100
 1101            if (settings.IsRepredictMode)
 102            {
 1103                var maxValue = settings.MaxRepredictions ?? int.MaxValue;
 1104                _console.MarkupLine($"[yellow]Reprediction mode enabled - max repredictions: {(settings.MaxRepredictions
 105            }
 106
 107            // Execute the bonus prediction workflow
 1108            await ExecuteBonusWorkflow(settings);
 109
 1110            return 0;
 111        }
 1112        catch (Exception ex)
 113        {
 1114            _logger.LogError(ex, "Error executing bonus command");
 1115            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 1116            return 1;
 117        }
 1118    }
 119
 120    /// <summary>
 121    /// Communities that have production workflows invoking the bonus command.
 122    /// Update this set when adding or removing community bonus workflows in .github/workflows/.
 123    /// See .github/workflows/AGENTS.md for details.
 124    /// </summary>
 1125    private static readonly HashSet<string> ProductionCommunities = new(StringComparer.OrdinalIgnoreCase)
 1126    {
 1127        "schadensfresse",
 1128        "pes-squad",
 1129        "ehonda-ai-arena",
 1130        "rabetrabauken2026"
 1131    };
 132
 133    private async Task ExecuteBonusWorkflow(BaseSettings settings)
 134    {
 135        // Start root OTel activity for Langfuse trace
 1136        using var activity = Telemetry.Source.StartActivity("bonus");
 137
 138        // Set Langfuse environment based on community
 1139        var environment = ProductionCommunities.Contains(settings.Community) ? "production" : "development";
 1140        LangfuseActivityPropagation.SetEnvironment(activity, environment);
 141
 1142        string communityContext = settings.CommunityContext ?? settings.Community;
 1143        var competition = CompetitionResolver.ResolveCompetition(settings.Competition, settings.Community, communityCont
 1144        var modelConfig = PredictionServiceCommandSupport.CreateModelConfig(settings.Model, settings.ReasoningEffort);
 1145        var model = modelConfig.Model;
 1146        var repositoryCompetition = CompetitionResolver.ToRepositoryCompetitionArgument(competition);
 147
 148        // Set Langfuse trace-level attributes
 1149        var sessionId = $"bonus-{settings.Community}";
 1150        var traceTags = new[] { settings.Community, model, competition };
 1151        LangfuseActivityPropagation.SetSessionId(activity, sessionId);
 1152        LangfuseActivityPropagation.SetTraceTags(activity, traceTags);
 1153        LangfuseActivityPropagation.SetTraceMetadata(activity, "community", settings.Community);
 1154        LangfuseActivityPropagation.SetTraceMetadata(activity, "competition", competition);
 1155        LangfuseActivityPropagation.SetTraceMetadata(activity, "model", model);
 1156        if (modelConfig.ReasoningEffort is not null)
 157        {
 1158            LangfuseActivityPropagation.SetTraceMetadata(activity, "reasoningEffort", modelConfig.ReasoningEffort);
 159        }
 1160        LangfuseActivityPropagation.SetTraceMetadata(activity, "repredictMode", settings.IsRepredictMode ? "true" : "fal
 161
 162        // Note: trace input is set after bonus questions are fetched
 163
 164        // Create services using factories
 1165        var kicktippClient = _kicktippClientFactory.CreateClient();
 1166        var predictionService = PredictionServiceCommandSupport.CreatePredictionService(
 1167            _openAiServiceFactory,
 1168            _langfuseClient,
 1169            _console,
 1170            model,
 1171            competition,
 1172            settings.Community,
 1173            communityContext,
 1174            settings.PromptSource,
 1175            settings.LangfusePromptName,
 1176            settings.LangfusePromptLabel,
 1177            settings.LangfusePromptVersion,
 1178            modelConfig.ReasoningEffort,
 1179            settings.MaxOutputTokenCount,
 1180            bonusPrompt: true);
 181
 182        // Log the prompt paths being used
 1183        if (settings.Verbose)
 184        {
 1185            _console.MarkupLine($"[dim]Bonus prompt:[/] [blue]{predictionService.GetBonusPromptPath()}[/]");
 186        }
 187
 188        // Create KPI Context Provider for bonus predictions using factory
 1189        var kpiContextProvider = _contextProviderFactory.CreateKpiContextProvider(repositoryCompetition);
 1190        if (CompetitionResolver.IsWorldCupCompetition(competition))
 191        {
 1192            await EnsureWorldCupRankingKpiPresentAsync(kpiContextProvider, communityContext);
 193        }
 194
 1195        var tokenUsageTracker = _openAiServiceFactory.GetTokenUsageTracker();
 196
 197        // Create repositories
 1198        var predictionRepository = _firebaseServiceFactory.CreatePredictionRepository(repositoryCompetition);
 1199        var kpiRepository = _firebaseServiceFactory.CreateKpiRepository(repositoryCompetition);
 1200        var databaseEnabled = true;
 201
 202        // Reset token usage tracker for this workflow
 1203        tokenUsageTracker.Reset();
 204
 1205        LangfuseActivityPropagation.SetTraceMetadata(activity, "communityContext", communityContext);
 206
 1207        _console.MarkupLine($"[blue]Using community:[/] [yellow]{settings.Community}[/]");
 1208        _console.MarkupLine($"[blue]Using community context:[/] [yellow]{communityContext}[/]");
 1209        _console.MarkupLine($"[blue]Using competition:[/] [yellow]{competition}[/]");
 1210        _console.MarkupLine("[blue]Getting open bonus questions from Kicktipp...[/]");
 211
 212        // Step 1: Get open bonus questions from Kicktipp
 1213        var bonusQuestions = await kicktippClient.GetOpenBonusQuestionsAsync(settings.Community);
 214
 1215        if (!bonusQuestions.Any())
 216        {
 1217            _console.MarkupLine("[yellow]No open bonus questions found[/]");
 1218            return;
 219        }
 220
 1221        _console.MarkupLine($"[green]Found {bonusQuestions.Count} open bonus questions[/]");
 222
 223        // Set trace input now that we know the questions
 1224        var traceInput = new
 1225        {
 1226            community = settings.Community,
 1227            model,
 1228            competition,
 1229            questions = bonusQuestions.Select(q => q.Text).ToArray()
 1230        };
 1231        activity?.SetTag("langfuse.trace.input", JsonSerializer.Serialize(traceInput));
 232
 1233        if (databaseEnabled)
 234        {
 1235            _console.MarkupLine("[blue]Database enabled - checking for existing predictions...[/]");
 236        }
 237
 1238        var predictions = new Dictionary<string, BonusPrediction>();
 1239        var traceRepredictionIndices = new HashSet<string>(StringComparer.Ordinal);
 240
 241        // Step 2: For each question, check database first, then predict if needed
 1242        foreach (var question in bonusQuestions)
 243        {
 1244            _console.MarkupLine($"[cyan]Processing:[/] {Markup.Escape(question.Text)}");
 245
 246            try
 247            {
 1248                BonusPrediction? prediction = null;
 1249                bool fromDatabase = false;
 1250                bool shouldPredict = false;
 1251                int? predictionRepredictionIndex = settings.IsRepredictMode ? null : 0;
 252
 253                // Check if we have an existing prediction in the database
 1254                if (databaseEnabled && !settings.OverrideDatabase && !settings.IsRepredictMode)
 255                {
 256                    // Look for prediction by question text, model, and community context
 1257                    prediction = await predictionRepository!.GetBonusPredictionByTextAsync(question.Text, modelConfig, c
 1258                    if (prediction != null)
 259                    {
 1260                        fromDatabase = true;
 1261                        if (settings.Agent)
 262                        {
 1263                            _console.MarkupLine($"[green]  ✓ Found existing prediction[/] [dim](from database)[/]");
 264                        }
 265                        else
 266                        {
 1267                            var optionTexts = question.Options
 1268                                .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1269                                .Select(o => o.Text);
 1270                            _console.MarkupLine($"[green]  ✓ Found existing prediction:[/] {string.Join(", ", optionText
 271                        }
 272                    }
 273                }
 274
 275                // Handle reprediction logic
 1276                if (settings.IsRepredictMode && databaseEnabled)
 277                {
 1278                    var currentRepredictionIndex = await predictionRepository!.GetBonusRepredictionIndexAsync(question.T
 279
 1280                    if (currentRepredictionIndex == -1)
 281                    {
 282                        // No prediction exists yet - create first prediction
 1283                        shouldPredict = true;
 1284                        predictionRepredictionIndex = 0;
 1285                        _console.MarkupLine($"[yellow]  → No existing prediction found, creating first prediction...[/]"
 286                    }
 287                    else
 288                    {
 289                        // Check if we can create another reprediction
 1290                        var maxAllowed = settings.MaxRepredictions ?? int.MaxValue;
 1291                        var nextIndex = currentRepredictionIndex + 1;
 292
 1293                        if (nextIndex <= maxAllowed)
 294                        {
 1295                            var isOutdated = await CheckBonusPredictionOutdated(
 1296                                predictionRepository!,
 1297                                kpiRepository,
 1298                                question.Text,
 1299                                modelConfig,
 1300                                communityContext,
 1301                                settings.Verbose);
 302
 1303                            if (isOutdated)
 304                            {
 1305                                shouldPredict = true;
 1306                                predictionRepredictionIndex = nextIndex;
 1307                                _console.MarkupLine($"[yellow]  → Creating reprediction {nextIndex} (current: {currentRe
 308                            }
 309                            else
 310                            {
 1311                                traceRepredictionIndices.Add(currentRepredictionIndex.ToString());
 1312                                _console.MarkupLine($"[green]  ✓ Skipped reprediction - current prediction is up-to-date
 313
 1314                                prediction = await predictionRepository!.GetBonusPredictionByTextAsync(question.Text, mo
 1315                                if (prediction != null)
 316                                {
 1317                                    fromDatabase = true;
 1318                                    if (!settings.Agent)
 319                                    {
 1320                                        var optionTexts = question.Options
 1321                                            .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1322                                            .Select(o => o.Text);
 1323                                        _console.MarkupLine($"[green]  ✓ Latest prediction:[/] {string.Join(", ", option
 324                                    }
 325                                }
 326                            }
 327                        }
 328                        else
 329                        {
 1330                            traceRepredictionIndices.Add(currentRepredictionIndex.ToString());
 1331                            _console.MarkupLine($"[yellow]  ✗ Skipped - already at max repredictions ({currentRepredicti
 332
 333                            // Get the latest prediction for display purposes
 1334                            prediction = await predictionRepository!.GetBonusPredictionByTextAsync(question.Text, modelC
 1335                            if (prediction != null)
 336                            {
 1337                                fromDatabase = true;
 1338                                if (!settings.Agent)
 339                                {
 1340                                    var optionTexts = question.Options
 1341                                        .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1342                                        .Select(o => o.Text);
 1343                                    _console.MarkupLine($"[green]  ✓ Latest prediction:[/] {string.Join(", ", optionText
 344                                }
 345                            }
 346                        }
 347                    }
 348                }
 349
 350                // If no existing prediction (normal mode) or we need to predict (reprediction mode), generate a new one
 1351                if (prediction == null || shouldPredict)
 352                {
 1353                    _console.MarkupLine($"[yellow]  → Generating new prediction...[/]");
 354
 355                    // Step 3: Get KPI context for bonus predictions
 1356                    var contextDocuments = new List<DocumentContext>();
 357
 358                    // Use KPI documents as context for bonus predictions (targeted by question content)
 1359                    await foreach (var context in kpiContextProvider.GetBonusQuestionContextAsync(question.Text, communi
 360                    {
 1361                        contextDocuments.Add(context);
 362                    }
 363
 1364                    if (settings.Verbose)
 365                    {
 1366                        _console.MarkupLine($"[dim]    Using {contextDocuments.Count} KPI context documents[/]");
 367                    }
 368
 1369                    var telemetryMetadata = new PredictionTelemetryMetadata(
 1370                        RepredictionIndex: predictionRepredictionIndex);
 371
 372                    // Predict the bonus question
 1373                    prediction = await predictionService.PredictBonusQuestionAsync(question, contextDocuments, telemetry
 374
 1375                    if (prediction != null)
 376                    {
 1377                        if (predictionRepredictionIndex.HasValue)
 378                        {
 1379                            traceRepredictionIndices.Add(predictionRepredictionIndex.Value.ToString());
 380                        }
 381
 1382                        if (settings.Agent)
 383                        {
 1384                            _console.MarkupLine($"[green]  ✓ Generated prediction[/]");
 385                        }
 386                        else
 387                        {
 1388                            var optionTexts = question.Options
 1389                                .Where(o => prediction.SelectedOptionIds.Contains(o.Id))
 1390                                .Select(o => o.Text);
 1391                            _console.MarkupLine($"[green]  ✓ Generated prediction:[/] {string.Join(", ", optionTexts)}")
 392                        }
 393
 394                        // Save to database immediately if enabled
 1395                        if (databaseEnabled && !settings.DryRun)
 396                        {
 397                            try
 398                            {
 399                                // Get token usage and cost information
 1400                                var cost = (double)tokenUsageTracker.GetLastCost(); // Get the cost for this individual 
 401                                // Use the new GetLastUsageJson method to get full JSON
 1402                                var tokenUsageJson = tokenUsageTracker.GetLastUsageJson() ?? "{}";
 403
 1404                                if (settings.IsRepredictMode)
 405                                {
 406                                    // Save as reprediction with specific index
 1407                                    var currentIndex = await predictionRepository!.GetBonusRepredictionIndexAsync(questi
 1408                                    var nextIndex = currentIndex == -1 ? 0 : currentIndex + 1;
 409
 1410                                    await predictionRepository!.SaveBonusRepredictionAsync(
 1411                                        question,
 1412                                        prediction,
 1413                                        modelConfig,
 1414                                        tokenUsageJson,
 1415                                        cost,
 1416                                        communityContext,
 1417                                        contextDocuments.Select(d => d.Name),
 1418                                        nextIndex);
 419
 1420                                    if (settings.Verbose)
 421                                    {
 1422                                        _console.MarkupLine($"[dim]    ✓ Saved as reprediction {nextIndex} to database[/
 423                                    }
 424                                }
 425                                else
 426                                {
 427                                    // Save normally (override or new prediction)
 1428                                    await predictionRepository!.SaveBonusPredictionAsync(
 1429                                        question,
 1430                                        prediction,
 1431                                        modelConfig,
 1432                                        tokenUsageJson,
 1433                                        cost,
 1434                                        communityContext,
 1435                                        contextDocuments.Select(d => d.Name),
 1436                                        overrideCreatedAt: settings.OverrideDatabase);
 437
 1438                                    if (settings.Verbose)
 439                                    {
 1440                                        _console.MarkupLine($"[dim]    ✓ Saved to database[/]");
 441                                    }
 442                                }
 1443                            }
 1444                            catch (Exception ex)
 445                            {
 1446                                _logger.LogError(ex, "Failed to save bonus prediction for question '{QuestionText}'", qu
 1447                                _console.MarkupLine($"[red]    ✗ Failed to save to database: {ex.Message}[/]");
 1448                            }
 449                        }
 1450                        else if (databaseEnabled && settings.DryRun && settings.Verbose)
 451                        {
 1452                            _console.MarkupLine($"[dim]    (Dry run - skipped database save)[/]");
 453                        }
 454
 455                        // Show individual question token usage in verbose mode
 1456                        if (settings.Verbose)
 457                        {
 1458                            var questionUsage = !string.IsNullOrEmpty(settings.EstimatedCostsModel)
 1459                                ? tokenUsageTracker.GetLastUsageCompactSummaryWithEstimatedCosts(settings.EstimatedCosts
 1460                                : tokenUsageTracker.GetLastUsageCompactSummary();
 1461                            _console.MarkupLine($"[dim]    Token usage: {questionUsage}[/]");
 462                        }
 463                    }
 464                    else
 465                    {
 1466                        _console.MarkupLine($"[red]  ✗ Failed to generate prediction[/]");
 1467                        continue;
 468                    }
 1469                }
 470
 1471                predictions[question.FormFieldName ?? question.Text] = prediction;
 472
 1473                if (!fromDatabase && settings.Verbose)
 474                {
 1475                    _console.MarkupLine($"[dim]    Ready for Kicktipp placement[/]");
 476                }
 1477            }
 1478            catch (Exception ex)
 479            {
 1480                _logger.LogError(ex, "Error processing bonus question '{QuestionText}'", question.Text);
 1481                _console.MarkupLine($"[red]  ✗ Error processing question: {ex.Message}[/]");
 1482            }
 1483        }
 484
 1485        if (traceRepredictionIndices.Count > 0)
 486        {
 1487            LangfuseActivityPropagation.SetTraceMetadata(activity, "repredictionIndices", PredictionTelemetryMetadata.Bu
 1488            LangfuseActivityPropagation.SetTraceMetadata(activity, "hasRepredictions", traceRepredictionIndices.Any(inde
 489        }
 490
 1491        if (!predictions.Any())
 492        {
 1493            _console.MarkupLine("[yellow]No predictions available, nothing to place[/]");
 1494            activity?.SetTag("langfuse.trace.output", JsonSerializer.Serialize(new { error = "No predictions available" 
 1495            return;
 496        }
 497
 498        // Set trace output with all bonus predictions
 1499        var traceOutput = predictions.Select(p => new
 1500        {
 1501            question = p.Key,
 1502            selectedOptionIds = p.Value.SelectedOptionIds
 1503        }).ToArray();
 1504        activity?.SetTag("langfuse.trace.output", JsonSerializer.Serialize(traceOutput));
 505
 506        // Step 4: Place all predictions using PlaceBonusPredictionsAsync
 1507        _console.MarkupLine($"[blue]Placing {predictions.Count} bonus predictions to Kicktipp...[/]");
 508
 1509        if (settings.DryRun)
 510        {
 1511            _console.MarkupLine($"[magenta]✓ Dry run mode - would have placed {predictions.Count} bonus predictions (no 
 512        }
 513        else
 514        {
 1515            var success = await kicktippClient.PlaceBonusPredictionsAsync(settings.Community, predictions, overridePredi
 516
 1517            if (success)
 518            {
 1519                _console.MarkupLine($"[green]✓ Successfully placed all {predictions.Count} bonus predictions![/]");
 520            }
 521            else
 522            {
 1523                _console.MarkupLine("[red]✗ Failed to place some or all bonus predictions[/]");
 524            }
 525        }
 526
 527        // Display token usage summary
 1528        var summary = !string.IsNullOrEmpty(settings.EstimatedCostsModel)
 1529            ? tokenUsageTracker.GetCompactSummaryWithEstimatedCosts(settings.EstimatedCostsModel)
 1530            : tokenUsageTracker.GetCompactSummary();
 1531        _console.MarkupLine($"[dim]Token usage (uncached/cached/reasoning/output/$cost): {summary}[/]");
 1532    }
 533
 534    private static async Task EnsureWorldCupRankingKpiPresentAsync(
 535        IKpiContextProvider kpiContextProvider,
 536        string communityContext)
 537    {
 1538        await foreach (var context in kpiContextProvider.GetContextAsync(communityContext))
 539        {
 1540            if (string.Equals(context.Name, FifaRankingsDocumentName, StringComparison.OrdinalIgnoreCase))
 541            {
 542                return;
 543            }
 544        }
 545
 1546        throw new InvalidOperationException(
 1547            "Missing required WM26 KPI context document 'fifa-rankings'. " +
 1548            "Run collect-context fifa for this community context.");
 1549    }
 550
 551    private async Task<bool> CheckBonusPredictionOutdated(
 552        IPredictionRepository predictionRepository,
 553        IKpiRepository kpiRepository,
 554        string questionText,
 555        PredictionModelConfig modelConfig,
 556        string communityContext,
 557        bool verbose)
 558    {
 559        try
 560        {
 1561            var predictionMetadata = await predictionRepository.GetBonusPredictionMetadataByTextAsync(
 1562                questionText, modelConfig, communityContext);
 563
 1564            if (predictionMetadata == null)
 565            {
 0566                return false;
 567            }
 568
 1569            foreach (var contextDocumentName in predictionMetadata.ContextDocumentNames)
 570            {
 1571                var kpiDocument = await kpiRepository.GetKpiDocumentAsync(contextDocumentName, communityContext);
 1572                if (kpiDocument != null)
 573                {
 1574                    if (kpiDocument.CreatedAt > predictionMetadata.CreatedAt)
 575                    {
 1576                        if (verbose)
 577                        {
 1578                            _console.MarkupLine($"[yellow]KPI document '{contextDocumentName}' updated after prediction 
 1579                            _console.MarkupLine($"  [dim]Prediction created:[/] {predictionMetadata.CreatedAt:yyyy-MM-dd
 1580                            _console.MarkupLine($"  [dim]KPI document created:[/] {kpiDocument.CreatedAt:yyyy-MM-dd HH:m
 581                        }
 582
 1583                        return true;
 584                    }
 585
 1586                    if (verbose)
 587                    {
 0588                        _console.MarkupLine($"[dim]KPI document '{contextDocumentName}' found, version {kpiDocument.Vers
 589                    }
 590                }
 0591                else if (verbose)
 592                {
 0593                    _console.MarkupLine($"[yellow]Warning: KPI document '{contextDocumentName}' not found[/]");
 594                }
 1595            }
 596
 1597            return false;
 598        }
 0599        catch (Exception ex)
 600        {
 0601            if (verbose)
 602            {
 0603                _console.MarkupLine($"[yellow]Warning: Could not check if prediction is outdated: {ex.Message}[/]");
 604            }
 605
 0606            return false;
 607        }
 1608    }
 609}