using AIStudio.Chat; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.Security; using AIStudio.Tools.Services; namespace AIStudio.Assistants.ModelComparison; /// /// Asks the judge of a comparison series about the two chats of a task, once the user decided. /// /// /// The judge reads both chats as transcripts, see ChatTranscript, after they passed the prompt injection filter, and /// only when both fit into its context window together: a judge which sees only the beginning of a chat would judge /// something else than the user did. Without a known context window, it is asked anyway, and a provider which refuses /// the length leaves the task without a verdict. /// /// The settings. /// The prompt injection filter. /// Counts what the judge would read. /// The logger. public sealed class ModelComparisonSeriesJudge(SettingsManager settingsManager, PromptInjectionGuardService guard, ConversationTokenCounter tokenCounter, ILogger logger) { /// /// How many tokens of the context window stay free for the answer of the judge. /// private const int ANSWER_RESERVE_TOKENS = 1_000; /// /// Asks the judge about a decided task and notes its verdict, or why there is none, in the task. /// /// The series, with its judge. /// The decided task. /// The first chat of the task, or null when it cannot be read. /// The second chat of the task, or null when it cannot be read. /// The title of the assistant, as the source the report of the filter names. /// Cancels the judge; the task stays as it was then. public async Task JudgeAsync(ModelComparisonSeries series, ModelComparisonSeriesTask task, ChatThread? chatOne, ChatThread? chatTwo, string title, CancellationToken token) { if (series.Judge is not { } judgeIdentity) return; var judgeProvider = ModelComparisonProviderCheck.ProviderAsItWas(settingsManager, series.JudgeProviderId, judgeIdentity); if (judgeProvider.UsedLLMProvider is LLMProviders.NONE || ModelComparisonProviderCheck.IssueOf(settingsManager, judgeProvider) is not null) { task.SkipJudge(ModelComparisonSeriesJudgeStatus.UNAVAILABLE); return; } if (chatOne is null || chatTwo is null) { task.SkipJudge(ModelComparisonSeriesJudgeStatus.NO_VERDICT); return; } var transcriptOfChatOne = ChatTranscript.Of(chatOne); var transcriptOfChatTwo = ChatTranscript.Of(chatTwo); var languageName = ModelComparisonFormat.LanguageName; // The filter only removes text, so the prompt before it is the longest the judge can get: if (!await this.FitsAsync(judgeProvider, new ModelComparisonChatJudgeRequest(transcriptOfChatOne, transcriptOfChatTwo, languageName, series.JudgeInstructions).ToPrompt(), token)) { task.SkipJudge(ModelComparisonSeriesJudgeStatus.TOO_LONG); return; } // One action per task: the user hears once, when the judge is done, what the filter removed from the chats. await using var reportingScope = guard.BeginAction(); var runner = new ModelComparisonRunner(logger, ModelComparisonProviderAdapter.CreateJudgeInputFilter(guard, reportingScope, title)); var judge = ModelComparisonProviderAdapter.CreateParticipant(judgeProvider, settingsManager, Tools.Components.MODEL_COMPARISON_ASSISTANT, title); var verdict = await runner.JudgeChatsAsync(judge, transcriptOfChatOne, transcriptOfChatTwo, languageName, series.JudgeInstructions, token); // A canceled judge has no verdict, but it was not the fault of the judge either: token.ThrowIfCancellationRequested(); task.TakeVerdict(verdict); } /// /// Checks whether a prompt fits into the context window of the judge, with room for its answer. /// /// The provider of the judge. /// The prompt. /// The cancellation token. /// True when it fits, or when nobody knows. private async Task FitsAsync(AIStudio.Settings.Provider judgeProvider, string prompt, CancellationToken token) { var contextWindow = judgeProvider.GetModelProfile().Context; if (!contextWindow.IsKnown) return true; try { return await tokenCounter.CountTextAsync(judgeProvider, prompt, token) + ANSWER_RESERVE_TOKENS <= contextWindow.DefaultTokens; } catch (InvalidOperationException exception) { logger.LogWarning(exception, "Could not count the tokens of the chats for the judge of a comparison series. Asking the judge anyway."); return true; } } }