From b7255694cd1ab4d4c85a9815b69c23e2232a5870 Mon Sep 17 00:00:00 2001 From: Dominic Neuburg Date: Sat, 14 Nov 2026 14:11:34 +0100 Subject: [PATCH] Added the Model Comparison assistant for blind comparisons of two models (#1009) Co-authored-by: Thorsten Sommer --- .../Assistants/AssistantBase.razor | 6 +- .../Assistants/AssistantBase.razor.cs | 14 +- .../Assistants/I18N/allTexts.lua | 1353 +++++++++++++++++ .../AssistantModelComparison.razor | 149 ++ .../AssistantModelComparison.razor.cs | 850 +++++++++++ .../AssistantModelComparisonTasks.razor | 92 ++ .../AssistantModelComparisonTasks.razor.cs | 211 +++ .../ModelComparison/ModelComparisonAccess.cs | 24 + .../ModelComparison/ModelComparisonAnswer.cs | 37 + .../ModelComparisonAnswerCaption.razor | 22 + .../ModelComparisonAnswerCaption.razor.cs | 56 + .../ModelComparisonAnswerCard.razor | 18 + .../ModelComparisonAnswerCard.razor.cs | 72 + .../ModelComparisonAssessment.razor | 6 + .../ModelComparisonAssessment.razor.cs | 45 + .../ModelComparison/ModelComparisonBatch.cs | 224 +++ .../ModelComparisonBatchEntry.cs | 16 + .../ModelComparisonBatchRunner.cs | 127 ++ .../ModelComparisonBatchSummary.razor | 75 + .../ModelComparisonBatchSummary.razor.cs | 61 + .../ModelComparisonBatchView.razor | 49 + .../ModelComparisonBatchView.razor.cs | 71 + .../ModelComparison/ModelComparisonCharts.cs | 74 + .../ModelComparisonChatJudgeRequest.cs | 42 + .../ModelComparisonEffortCharts.razor | 23 + .../ModelComparisonEffortCharts.razor.cs | 70 + .../ModelComparison/ModelComparisonFormat.cs | 68 + .../ModelComparison/ModelComparisonJson.cs | 31 + .../ModelComparisonJsonContext.cs | 14 + .../ModelComparisonJudgeAgreement.cs | 27 + .../ModelComparisonJudgeAnswer.cs | 21 + .../ModelComparisonJudgeExpectations.razor | 16 + .../ModelComparisonJudgeExpectations.razor.cs | 109 ++ .../ModelComparisonJudgeFormat.cs | 85 ++ .../ModelComparisonJudgeReasoning.razor | 8 + .../ModelComparisonJudgeReasoning.razor.cs | 46 + .../ModelComparisonJudgeRequest.cs | 87 ++ .../ModelComparisonJudgeVerdict.cs | 98 ++ .../ModelComparisonJudgeVerdictCard.razor | 26 + .../ModelComparisonJudgeVerdictCard.razor.cs | 60 + .../ModelComparison/ModelComparisonMode.cs | 17 + .../ModelComparisonModeChoice.razor | 22 + .../ModelComparisonModeChoice.razor.cs | 54 + .../ModelComparisonModeExtensions.cs | 45 + .../ModelComparisonModelChoice.cs | 17 + .../ModelComparisonModelIdentity.cs | 61 + .../ModelComparisonModelKey.cs | 35 + .../ModelComparisonModelSpeed.cs | 32 + .../ModelComparisonModelStanding.cs | 33 + .../ModelComparison/ModelComparisonNames.cs | 83 + .../ModelComparison/ModelComparisonOutcome.cs | 81 + .../ModelComparisonPairCard.razor | 45 + .../ModelComparisonPairCard.razor.cs | 66 + .../ModelComparison/ModelComparisonPairKey.cs | 19 + .../ModelComparison/ModelComparisonPairing.cs | 46 + .../ModelComparisonParticipant.cs | 22 + .../ModelComparisonPreselection.cs | 75 + .../ModelComparisonPresentationOrder.cs | 14 + ...elComparisonPresentationOrderExtensions.cs | 48 + .../ModelComparison/ModelComparisonPreset.cs | 52 + .../ModelComparisonPresetChoice.cs | 30 + .../ModelComparisonProviderAdapter.cs | 113 ++ .../ModelComparisonProviderCheck.cs | 49 + .../ModelComparisonQuestionPreview.cs | 30 + .../ModelComparisonRanking.razor | 32 + .../ModelComparisonRanking.razor.cs | 47 + .../ModelComparison/ModelComparisonRecord.cs | 88 ++ .../ModelComparisonRecordAnswer.cs | 35 + .../ModelComparisonRecordRun.cs | 28 + .../ModelComparison/ModelComparisonRequest.cs | 77 + .../ModelComparisonResultSource.cs | 23 + .../ModelComparisonResultTexts.cs | 45 + .../ModelComparisonRunPanel.razor | 38 + .../ModelComparisonRunPanel.razor.cs | 63 + .../ModelComparisonRunProgress.razor | 47 + .../ModelComparisonRunProgress.razor.cs | 47 + .../ModelComparisonRunResult.cs | 42 + .../ModelComparisonRunStatus.cs | 37 + .../ModelComparison/ModelComparisonRunner.cs | 211 +++ .../ModelComparison/ModelComparisonSeries.cs | 121 ++ .../ModelComparisonSeriesChat.cs | 37 + .../ModelComparisonSeriesChatStore.cs | 18 + .../ModelComparisonSeriesCopy.cs | 100 ++ .../ModelComparisonSeriesDataSources.cs | 16 + .../ModelComparisonSeriesDecision.razor | 20 + .../ModelComparisonSeriesDecision.razor.cs | 52 + .../ModelComparisonSeriesEffort.cs | 28 + .../ModelComparisonSeriesJudge.cs | 101 ++ .../ModelComparisonSeriesJudgeStatus.cs | 33 + .../ModelComparisonSeriesPart.cs | 14 + .../ModelComparisonSeriesResult.razor | 68 + .../ModelComparisonSeriesResult.razor.cs | 45 + .../ModelComparisonSeriesSetupLine.razor | 16 + .../ModelComparisonSeriesSetupLine.razor.cs | 110 ++ .../ModelComparisonSeriesStatistics.cs | 97 ++ .../ModelComparisonSeriesTask.cs | 223 +++ .../ModelComparisonSeriesTaskState.cs | 27 + .../ModelComparisonSeriesTexts.cs | 35 + .../ModelComparisonSeriesThreads.cs | 81 + .../ModelComparisonSeriesVerdict.razor | 48 + .../ModelComparisonSeriesVerdict.razor.cs | 68 + .../ModelComparisonSeriesView.razor | 114 ++ .../ModelComparisonSeriesView.razor.cs | 496 ++++++ .../ModelComparisonShownChat.cs | 28 + .../ModelComparisonSignificance.cs | 22 + .../ModelComparisonSpeeds.razor | 30 + .../ModelComparisonSpeeds.razor.cs | 50 + .../ModelComparisonStatistics.cs | 94 ++ .../ModelComparisonStatisticsBuilder.cs | 307 ++++ .../ModelComparisonStorageOptions.cs | 12 + .../ModelComparisonStore.Presets.cs | 99 ++ .../ModelComparisonStore.Results.cs | 105 ++ .../ModelComparisonStore.Series.cs | 305 ++++ .../ModelComparison/ModelComparisonStore.cs | 163 ++ .../ModelComparison/ModelComparisonTally.cs | 64 + .../ModelComparison/ModelComparisonTexts.cs | 34 + .../ModelComparison/ModelComparisonVote.cs | 15 + .../ModelComparisonVoteColumns.razor | 44 + .../ModelComparisonVoteColumns.razor.cs | 64 + .../ModelComparisonVoting.razor | 49 + .../ModelComparisonVoting.razor.cs | 52 + .../ModelComparisonWinShare.cs | 95 ++ .../Translation/AssistantTranslation.razor | 3 +- app/MindWork AI Studio/Chat/ChatTranscript.cs | 79 + app/MindWork AI Studio/Chat/ContentText.cs | 10 +- .../Chat/Embedded/EmbeddedChatOpening.cs | 22 + .../Chat/Embedded/EmbeddedChatRequest.cs | 39 + .../Chat/Embedded/EmbeddedChatService.cs | 336 ++++ .../Chat/Embedded/EmbeddedConversation.cs | 57 + .../Chat/Embedded/IEmbeddedChatStore.cs | 28 + .../Components/AssistantModeChoice.razor | 30 + .../Components/AssistantModeChoice.razor.cs | 28 + .../Components/AssistantModeOption.cs | 11 + .../Components/CompactExpansionPanel.razor | 2 +- .../Components/CompactExpansionPanel.razor.cs | 10 + .../ConfigurationProviderSelection.razor | 4 +- .../ConfigurationProviderSelection.razor.cs | 23 + .../Components/DataFolders.razor.cs | 1 + .../Components/DataSourceSelection.razor | 7 +- .../Components/DataSourceSelection.razor.cs | 89 +- .../Components/DebouncedTextField.razor | 3 +- .../Components/DebouncedTextField.razor.cs | 25 +- .../Components/EmbeddedChat.razor | 67 + .../Components/EmbeddedChat.razor.cs | 336 ++++ .../ProviderSelectionWithConfidence.razor | 13 + .../ProviderSelectionWithConfidence.razor.cs | 32 + .../Components/ToolSelection.razor.cs | 29 +- .../ModelComparisonResultsDialog.razor | 165 ++ .../ModelComparisonResultsDialog.razor.cs | 174 +++ .../Dialogs/ModelComparisonSeriesDialog.razor | 96 ++ .../ModelComparisonSeriesDialog.razor.cs | 382 +++++ .../SettingsDialogModelComparison.razor | 29 + .../SettingsDialogModelComparison.razor.cs | 82 + app/MindWork AI Studio/Pages/Assistants.razor | 1 + .../Plugins/configuration/plugin.lua | 20 +- .../plugin.lua | 1353 +++++++++++++++++ .../plugin.lua | 1353 +++++++++++++++++ app/MindWork AI Studio/Program.cs | 5 + .../Provider/BaseProvider.cs | 57 +- app/MindWork AI Studio/Provider/IProvider.cs | 13 +- app/MindWork AI Studio/Provider/NoProvider.cs | 3 + .../Provider/ProviderIdentityExtensions.cs | 34 + app/MindWork AI Studio/Routes.razor.cs | 3 + .../Settings/ConfigurableAssistant.cs | 1 + .../Settings/DataModel/Data.cs | 7 +- .../Settings/DataModel/DataModelComparison.cs | 68 + app/MindWork AI Studio/Tools/AppIcons.cs | 54 + .../Tools/AssistantVisibilityExtensions.cs | 1 + app/MindWork AI Studio/Tools/Components.cs | 1 + .../Tools/ComponentsExtensions.cs | 7 + .../Tools/DataBackup/DataBackupContents.cs | 1 + .../Tools/DataBackup/DataBackupEntry.cs | 1 + .../Tools/DataBackup/DataBackupEntryKind.cs | 5 + .../Tools/PluginSystem/PluginConfiguration.cs | 6 + .../Services/ProvenanceCleanupService.cs | 30 +- .../Tools/Services/StoredChatLocation.cs | 15 +- .../Tools/SharedSelectionRules.cs | 94 ++ .../SemanticSearch/SemanticSearchTool.cs | 9 +- .../wwwroot/changelog/v26.11.2.md | 6 +- .../ChatSystemPromptOfAssistantsTests.cs | 2 + .../ModelComparisonBatchRunnerTests.cs | 259 ++++ .../ModelComparisonBatchTests.cs | 340 +++++ .../ModelComparisonChartsTests.cs | 25 + .../ModelComparisonJudgeExpectationsTests.cs | 31 + .../ModelComparisonJudgeFormatTests.cs | 97 ++ .../ModelComparisonJudgeRequestTests.cs | 103 ++ .../ModelComparisonJudgeVerdictTests.cs | 79 + .../ModelComparisonNamesTests.cs | 93 ++ .../ModelComparisonOutcomeTests.cs | 74 + .../ModelComparisonPreselectionTests.cs | 134 ++ .../ModelComparisonPresentationOrderTests.cs | 104 ++ .../ModelComparisonPresetChoiceTests.cs | 55 + .../ModelComparisonProviderAdapterTests.cs | 58 + .../ModelComparisonRecordTests.cs | 127 ++ .../ModelComparisonRequestTests.cs | 163 ++ .../ModelComparisonRunnerTests.cs | 513 +++++++ .../ModelComparisonSeriesCopyTests.cs | 239 +++ .../ModelComparisonSeriesStatisticsTests.cs | 138 ++ .../ModelComparisonSeriesTaskTests.cs | 160 ++ .../ModelComparisonSeriesThreadsTests.cs | 89 ++ .../ModelComparisonShownChatTests.cs | 61 + .../ModelComparisonStatisticsTests.cs | 274 ++++ .../ModelComparisonStorePresetTests.cs | 168 ++ .../ModelComparisonStoreResultTests.cs | 163 ++ .../ModelComparisonStoreSeriesTests.cs | 252 +++ .../ModelComparisonTallyTests.cs | 61 + .../ModelComparisonWinShareTests.cs | 71 + app/Tests/Chat/ChatTranscriptTests.cs | 77 + .../Chat/Embedded/EmbeddedChatOpeningTests.cs | 71 + .../Chat/Embedded/EmbeddedChatServiceTests.cs | 240 +++ .../Embedded/InMemoryEmbeddedChatStore.cs | 46 + app/Tests/Provider/ProviderIdentityTests.cs | 38 + .../JsonTestRoots.ModelAnswers.cs | 2 + app/Tests/Serialization/JsonTestRoots.cs | 6 + .../Tools/DataBackup/DataBackupWriterTests.cs | 2 + .../Tools/ProvenanceCleanupServiceTests.cs | 29 +- app/Tests/Tools/SharedSelectionRulesTests.cs | 159 ++ .../EffectiveRetrievalModeTests.cs | 15 + .../SemanticSearchToolDefinitionTests.cs | 49 +- documentation/Tools.md | 2 + runtime/src/data_restore.rs | 18 +- 221 files changed, 19989 insertions(+), 90 deletions(-) create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAccess.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswer.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatch.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchEntry.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchRunner.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonCharts.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonChatJudgeRequest.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonFormat.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJson.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJsonContext.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAgreement.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAnswer.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeFormat.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeRequest.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdict.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonMode.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelChoice.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelIdentity.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelKey.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelSpeed.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelStanding.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonNames.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonOutcome.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairKey.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairing.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonParticipant.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreselection.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrder.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrderExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreset.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresetChoice.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderAdapter.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderCheck.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonQuestionPreview.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecord.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordAnswer.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordRun.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRequest.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultSource.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultTexts.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunResult.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunStatus.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunner.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeries.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChat.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChatStore.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesCopy.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDataSources.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesEffort.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudge.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudgeStatus.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesPart.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesStatistics.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTask.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTaskState.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTexts.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesThreads.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonShownChat.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSignificance.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatistics.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatisticsBuilder.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStorageOptions.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Presets.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Results.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Series.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTally.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTexts.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVote.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonWinShare.cs create mode 100644 app/MindWork AI Studio/Chat/ChatTranscript.cs create mode 100644 app/MindWork AI Studio/Chat/Embedded/EmbeddedChatOpening.cs create mode 100644 app/MindWork AI Studio/Chat/Embedded/EmbeddedChatRequest.cs create mode 100644 app/MindWork AI Studio/Chat/Embedded/EmbeddedChatService.cs create mode 100644 app/MindWork AI Studio/Chat/Embedded/EmbeddedConversation.cs create mode 100644 app/MindWork AI Studio/Chat/Embedded/IEmbeddedChatStore.cs create mode 100644 app/MindWork AI Studio/Components/AssistantModeChoice.razor create mode 100644 app/MindWork AI Studio/Components/AssistantModeChoice.razor.cs create mode 100644 app/MindWork AI Studio/Components/AssistantModeOption.cs create mode 100644 app/MindWork AI Studio/Components/EmbeddedChat.razor create mode 100644 app/MindWork AI Studio/Components/EmbeddedChat.razor.cs create mode 100644 app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor create mode 100644 app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor create mode 100644 app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor.cs create mode 100644 app/MindWork AI Studio/Provider/ProviderIdentityExtensions.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataModelComparison.cs create mode 100644 app/MindWork AI Studio/Tools/SharedSelectionRules.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonBatchRunnerTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonBatchTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonChartsTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonJudgeExpectationsTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonJudgeFormatTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonJudgeRequestTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonJudgeVerdictTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonNamesTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonOutcomeTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonPreselectionTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonPresentationOrderTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonPresetChoiceTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonProviderAdapterTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonRecordTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonRequestTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonRunnerTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonSeriesCopyTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonSeriesStatisticsTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonSeriesTaskTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonSeriesThreadsTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonShownChatTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonStatisticsTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonStorePresetTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonStoreResultTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonStoreSeriesTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonTallyTests.cs create mode 100644 app/Tests/Assistants/ModelComparison/ModelComparisonWinShareTests.cs create mode 100644 app/Tests/Chat/ChatTranscriptTests.cs create mode 100644 app/Tests/Chat/Embedded/EmbeddedChatOpeningTests.cs create mode 100644 app/Tests/Chat/Embedded/EmbeddedChatServiceTests.cs create mode 100644 app/Tests/Chat/Embedded/InMemoryEmbeddedChatStore.cs create mode 100644 app/Tests/Provider/ProviderIdentityTests.cs create mode 100644 app/Tests/Tools/SharedSelectionRulesTests.cs diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 548681cc6..57d58c532 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -27,9 +27,9 @@ - + @this.Description - + @if (this.Body is not null) { @@ -186,7 +186,7 @@ } - @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence) + @if (this.ShowsProviderConfidence) { } diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 42984e22a..1994a2572 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -133,6 +133,16 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected virtual bool ShowReset => true; + /// + /// Whether the footer shows the confidence card of the selected provider, and the submit button its confidence, + /// when the user wants to see confidences, see DataConfidence.ShowProviderConfidence. + /// + /// + /// An assistant which works with several providers at once turns this off: the footer would show only one of them. + /// It shows the confidence next to each provider selection instead, see ProviderSelectionWithConfidence. + /// + protected virtual bool ShowProviderConfidence => true; + /// /// Whether the system prompt starts with the current date and time. /// @@ -360,7 +370,9 @@ public abstract partial class AssistantBase : AssistantLowerBase wher /// private new string TA(string fallbackEN) => this.TA(fallbackEN, typeof(AssistantBase<>).Namespace, nameof(AssistantBase<>)); - private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty; + private bool ShowsProviderConfidence => this.ShowProviderConfidence && this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence; + + private string SubmitButtonStyle => this.ShowsProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty; private IReadOnlyList VisibleSendToAssistants => this.AssistantLaunchService.GetDestinationsFor(this.Component); diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 11f072a7f..fbc80a555 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4615,6 +4615,990 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133 -- Clear UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Clear" +-- Material: {0} characters +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1097973848"] = "Material: {0} characters" + +-- Judge (optional) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1206060603"] = "Judge (optional)" + +-- Give your preset a name which tells you later what it compares. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1326490469"] = "Give your preset a name which tells you later what it compares." + +-- Give your preset a name which tells you later what it compares. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1326490469::INFORMAL"] = "Give your preset a name which tells you later what it compares." + +-- Please provide a name for your preset. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1408294373"] = "Please provide a name for your preset." + +-- Please provide a name for your preset. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1408294373::INFORMAL"] = "Please provide a name for your preset." + +-- 20 (heavy load on the models) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1426001529"] = "20 (heavy load on the models)" + +-- Please provide a question or task for both models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1481551697"] = "Please provide a question or task for both models." + +-- Please provide a question or task for both models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1481551697::INFORMAL"] = "Please provide a question or task for both models." + +-- Delete this preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T148310729"] = "Delete this preset" + +-- Your request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1607440787"] = "Your request" + +-- Your request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1607440787::INFORMAL"] = "Your request" + +-- Your question or task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1646098156"] = "Your question or task" + +-- Your question or task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1646098156::INFORMAL"] = "Your question or task" + +-- Material (optional) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1771342343"] = "Material (optional)" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1990305431"] = "Results of all comparisons" + +-- Switch mode +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2045889708"] = "Switch mode" + +-- Add preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2059304803"] = "Add preset" + +-- 1 (single comparison) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2267053576"] = "1 (single comparison)" + +-- No judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2562272449"] = "No judge" + +-- AI Studio could not save the results of this comparison for the statistics. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2762349476"] = "AI Studio could not save the results of this comparison for the statistics." + +-- Preset from {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2841002060"] = "Preset from {0}" + +-- Models +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T30216107"] = "Models" + +-- The name of your preset must be between 6 and 60 characters long. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3065222761"] = "The name of your preset must be between 6 and 60 characters long." + +-- The name of your preset must be between 6 and 60 characters long. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3065222761::INFORMAL"] = "The name of your preset must be between 6 and 60 characters long." + +-- A judge assesses every run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3109664129"] = "A judge assesses every run" + +-- Are you sure you want to delete the preset '{0}'? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3246395362"] = "Are you sure you want to delete the preset '{0}'? This cannot be undone." + +-- Are you sure you want to delete the preset '{0}'? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3246395362::INFORMAL"] = "Are you sure you want to delete the preset '{0}'? This cannot be undone." + +-- Delete preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3276319725"] = "Delete preset" + +-- Material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3419754368"] = "Material" + +-- Compare +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3581545044"] = "Compare" + +-- Ask a judge? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3641185871"] = "Ask a judge?" + +-- Load material from a file +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3699307713"] = "Load material from a file" + +-- Expand +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3889190145"] = "Expand" + +-- Presets +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3897629751"] = "Presets" + +-- Preset name +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T40334785"] = "Preset name" + +-- A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4043921725"] = "A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself." + +-- A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4043921725::INFORMAL"] = "A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself." + +-- First model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4044223184"] = "First model" + +-- AI Studio could not save the change to your presets. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4152209182"] = "AI Studio could not save the change to your presets. Please try again." + +-- AI Studio could not save the change to your presets. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4152209182::INFORMAL"] = "AI Studio could not save the change to your presets. Please try again." + +-- Collapse +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T5885182"] = "Collapse" + +-- A preset with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T599249526"] = "A preset with this name already exists. Please choose a different name." + +-- A preset with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T599249526::INFORMAL"] = "A preset with this name already exists. Please choose a different name." + +-- Number of runs +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T752176293"] = "Number of runs" + +-- Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T764471416"] = "Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer." + +-- Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T764471416::INFORMAL"] = "Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer." + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T867467332"] = "Second model" + +-- Judge model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T953975257"] = "Judge model" + +-- Enter a new name for this comparison series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1211403772"] = "Enter a new name for this comparison series." + +-- Enter a new name for this comparison series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1211403772::INFORMAL"] = "Enter a new name for this comparison series." + +-- Open +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1401622761"] = "Open" + +-- Start a new series with this setup +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1452503898"] = "Start a new series with this setup" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1990305431"] = "Results of all comparisons" + +-- Delete series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2022370503"] = "Delete series" + +-- Switch mode +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2045889708"] = "Switch mode" + +-- Name of the series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2083243781"] = "Name of the series" + +-- Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2398358753"] = "Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone." + +-- Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2398358753::INFORMAL"] = "Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone." + +-- Your comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T242544328"] = "Your comparison series" + +-- Your comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T242544328::INFORMAL"] = "Your comparison series" + +-- AI Studio could not rename the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2698444055"] = "AI Studio could not rename the comparison series. Please try again." + +-- AI Studio could not rename the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2698444055::INFORMAL"] = "AI Studio could not rename the comparison series. Please try again." + +-- Back to all series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2733157589"] = "Back to all series" + +-- Started {0}, decided tasks: {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2771954403"] = "Started {0}, decided tasks: {1}" + +-- Started {0}, decided tasks: {1}, result shown +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2970598131"] = "Started {0}, decided tasks: {1}, result shown" + +-- Rename +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3355849203"] = "Rename" + +-- Rename series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3550827604"] = "Rename series" + +-- AI Studio could not delete the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3650237868"] = "AI Studio could not delete the comparison series. Please try again." + +-- AI Studio could not delete the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3650237868::INFORMAL"] = "AI Studio could not delete the comparison series. Please try again." + +-- You have no comparison series yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T369101477"] = "You have no comparison series yet." + +-- You have no comparison series yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T369101477::INFORMAL"] = "You have no comparison series yet." + +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3910956815"] = "Model Comparison" + +-- New comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T4249192191"] = "New comparison series" + +-- Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T922224005"] = "Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one." + +-- Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T922224005::INFORMAL"] = "Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one." + +-- First token after {0}, complete after {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCAPTION::T1658271318"] = "First token after {0}, complete after {1}" + +-- Judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCAPTION::T2094884932"] = "Judge" + +-- This model did not answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCARD::T472607376"] = "This model did not answer." + +-- All {0} runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1508408704"] = "All {0} runs are done." + +-- Start a new request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1767930003"] = "Start a new request" + +-- Judge verdicts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1879874010"] = "Judge verdicts" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1990305431"] = "Results of all comparisons" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T248601509"] = "Equally good" + +-- {0} of {1} runs have no usable judge verdict. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2716994296"] = "{0} of {1} runs have no usable judge verdict. They are not counted above." + +-- {0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2766679987"] = "{0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above." + +-- {0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2766679987::INFORMAL"] = "{0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above." + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T360017507"] = "Your votes" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T360017507::INFORMAL"] = "Your votes" + +-- Done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T38691245"] = "Done." + +-- Answer A +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1420338340"] = "Answer A" + +-- Answer B +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1470671197"] = "Answer B" + +-- You skipped the remaining votes. The summary follows once the remaining runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1964601229"] = "You skipped the remaining votes. The summary follows once the remaining runs are done." + +-- You skipped the remaining votes. The summary follows once the remaining runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1964601229::INFORMAL"] = "You skipped the remaining votes. The summary follows once the remaining runs are done." + +-- Both models are answering. This can take a while. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T2709682697"] = "Both models are answering. This can take a while." + +-- Waiting for the next run... +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T758625750"] = "Waiting for the next run..." + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T1422468824"] = "Your messages on average" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T1422468824::INFORMAL"] = "Your messages on average" + +-- Median time +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T2152829620"] = "Median time" + +-- Describe your expectations, values, and goals to the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T1734361278"] = "Describe your expectations, values, and goals to the judge" + +-- Describe your expectations, values, and goals to the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T1734361278::INFORMAL"] = "Describe your expectations, values, and goals to the judge" + +-- Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2577739352"] = "Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right." + +-- Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2577739352::INFORMAL"] = "Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right." + +-- Your expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2641486288"] = "Your expectations, values, and goals" + +-- Your expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2641486288::INFORMAL"] = "Your expectations, values, and goals" + +-- Yes, an AI may decide for me what is good and right +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2805496671"] = "Yes, an AI may decide for me what is good and right" + +-- For example: I prefer short answers which get to the point. Technical terms are welcome, since I work in the field. An answer should weigh several views instead of taking a side. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T326873300"] = "For example: I prefer short answers which get to the point. Technical terms are welcome, since I work in the field. An answer should weigh several views instead of taking a side." + +-- Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T332317276"] = "Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do." + +-- Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T332317276::INFORMAL"] = "Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do." + +-- I confirm that I have read and understood the above +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T3683380716"] = "I confirm that I have read and understood the above" + +-- No, I will describe my expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T401061652"] = "No, I will describe my expectations, values, and goals" + +-- What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T588888121"] = "What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models." + +-- What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T588888121::INFORMAL"] = "What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models." + +-- The judge saw both answers as equally good, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T110452334"] = "The judge saw both answers as equally good, matching your own vote." + +-- The judge saw both answers as equally good, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T110452334::INFORMAL"] = "The judge saw both answers as equally good, matching your own vote." + +-- The judge preferred {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1418012290"] = "The judge preferred {0}." + +-- The judge preferred {0}, differing from your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1499258629"] = "The judge preferred {0}, differing from your own vote." + +-- The judge preferred {0}, differing from your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1499258629::INFORMAL"] = "The judge preferred {0}, differing from your own vote." + +-- The judge saw both answers as equally good. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1679196260"] = "The judge saw both answers as equally good." + +-- The judge preferred {0}, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T2457939612"] = "The judge preferred {0}, matching your own vote." + +-- The judge preferred {0}, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T2457939612::INFORMAL"] = "The judge preferred {0}, matching your own vote." + +-- The judge saw both answers as equally good, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T3189061594"] = "The judge saw both answers as equally good, while you preferred one of them." + +-- The judge saw both answers as equally good, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T3189061594::INFORMAL"] = "The judge saw both answers as equally good, while you preferred one of them." + +-- Judge: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T4194679568"] = "Judge: {0}" + +-- The judge did not give a usable verdict. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T4278818434"] = "The judge did not give a usable verdict." + +-- Compare two models without knowing which one answers. Choose what you want to find out. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1060503021"] = "Compare two models without knowing which one answers. Choose what you want to find out." + +-- Compare two models without knowing which one answers. Choose what you want to find out. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1060503021::INFORMAL"] = "Compare two models without knowing which one answers. Choose what you want to find out." + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1990305431"] = "Results of all comparisons" + +-- Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T2256226870"] = "Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer." + +-- Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T2256226870::INFORMAL"] = "Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer." + +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T3910956815"] = "Model Comparison" + +-- Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T4217745718"] = "Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks." + +-- Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T4217745718::INFORMAL"] = "Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks." + +-- Model Comparison: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T2204151393"] = "Model Comparison: {0}" + +-- Everyday Tasks +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T2633045292"] = "Everyday Tasks" + +-- Answer Variance +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T3575446502"] = "Answer Variance" + +-- A series with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T1933751712"] = "A series with this name already exists. Please choose a different name." + +-- A series with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T1933751712::INFORMAL"] = "A series with this name already exists. Please choose a different name." + +-- Please give the series a name, so that you find it again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T2512778872"] = "Please give the series a name, so that you find it again." + +-- Please give the series a name, so that you find it again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T2512778872::INFORMAL"] = "Please give the series a name, so that you find it again." + +-- The name of your series may have at most {0} characters. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T4001587725"] = "The name of your series may have at most {0} characters." + +-- The name of your series may have at most {0} characters. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T4001587725::INFORMAL"] = "The name of your series may have at most {0} characters." + +-- {0} is clearly better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1539751573"] = "{0} is clearly better" + +-- Stored comparisons: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1864825956"] = "Stored comparisons: {0}" + +-- Judge verdicts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1879874010"] = "Judge verdicts" + +-- Assessment +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T2135313999"] = "Assessment" + +-- {0} vs. {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T2197341449"] = "{0} vs. {1}" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T248601509"] = "Equally good" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T360017507"] = "Your votes" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T360017507::INFORMAL"] = "Your votes" + +-- Not yet clear +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T877424713"] = "Not yet clear" + +-- The use of this provider has been prohibited by your organization. Please select a different provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1807387355"] = "The use of this provider has been prohibited by your organization. Please select a different provider." + +-- The use of this provider has been prohibited by your organization. Please select a different provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1807387355::INFORMAL"] = "The use of this provider has been prohibited by your organization. Please select a different provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1809312323"] = "Please select a provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1809312323::INFORMAL"] = "Please select a provider." + +-- This provider does not meet the confidence requirements of this assistant. Please choose another one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T512106357"] = "This provider does not meet the confidence requirements of this assistant. Please choose another one." + +-- This provider does not meet the confidence requirements of this assistant. Please choose another one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T512106357::INFORMAL"] = "This provider does not meet the confidence requirements of this assistant. Please choose another one." + +-- Clearly worse +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T1738164753"] = "Clearly worse" + +-- Assessment +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2135313999"] = "Assessment" + +-- Wins +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2168528702"] = "Wins" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2189814010"] = "Model" + +-- {0} to {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2854601881"] = "{0} to {1}" + +-- Losses +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3417493210"] = "Losses" + +-- Clearly better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3673745789"] = "Clearly better" + +-- Ties +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3724044438"] = "Ties" + +-- Share of wins +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3818681848"] = "Share of wins" + +-- Likely range +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T554551492"] = "Likely range" + +-- Not yet clear +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T877424713"] = "Not yet clear" + +-- “Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T1602691513"] = "“Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself." + +-- “Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T1602691513::INFORMAL"] = "“Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself." + +-- The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2051424113"] = "The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner." + +-- The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2051424113::INFORMAL"] = "The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner." + +-- The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2144465448"] = "The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you." + +-- The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2144465448::INFORMAL"] = "The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you." + +-- The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2456227034"] = "The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share." + +-- The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2456227034::INFORMAL"] = "The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share." + +-- The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T3079420033"] = "The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too." + +-- The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T3079420033::INFORMAL"] = "The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too." + +-- Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T584847906"] = "Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together." + +-- Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T584847906::INFORMAL"] = "Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together." + +-- Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T770148061"] = "Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them." + +-- Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T770148061::INFORMAL"] = "Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them." + +-- Run {0}: without your vote +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1440259155"] = "Run {0}: without your vote" + +-- Run {0}: without your vote +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1440259155::INFORMAL"] = "Run {0}: without your vote" + +-- Run {0}: you found both answers equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1728842697"] = "Run {0}: you found both answers equally good" + +-- Run {0}: you found both answers equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1728842697::INFORMAL"] = "Run {0}: you found both answers equally good" + +-- Run {0}: at least one model did not answer +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1775562627"] = "Run {0}: at least one model did not answer" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T248601509"] = "Equally good" + +-- First model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4044223184"] = "First model" + +-- Run {0}: you preferred {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4293630979"] = "Run {0}: you preferred {1}" + +-- Run {0}: you preferred {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4293630979::INFORMAL"] = "Run {0}: you preferred {1}" + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T867467332"] = "Second model" + +-- Run {0}: the judge gave no usable verdict, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1221245882"] = "Run {0}: the judge gave no usable verdict, your vote is missing." + +-- Run {0}: the judge gave no usable verdict, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1221245882::INFORMAL"] = "Run {0}: the judge gave no usable verdict, your vote is missing." + +-- Run {0}: no usable answer, nothing to vote on. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1407798402"] = "Run {0}: no usable answer, nothing to vote on." + +-- Run {0}: queued, waiting for a free slot. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1600973378"] = "Run {0}: queued, waiting for a free slot." + +-- Run {0}: still working. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1838060603"] = "Run {0}: still working." + +-- Run {0}: the models and the judge are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2115467037"] = "Run {0}: the models and the judge are done, your vote is missing." + +-- Run {0}: the models and the judge are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2115467037::INFORMAL"] = "Run {0}: the models and the judge are done, your vote is missing." + +-- Progress of the runs +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2330056762"] = "Progress of the runs" + +-- Run {0}: your vote was skipped. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2715329048"] = "Run {0}: your vote was skipped." + +-- Run {0}: your vote was skipped. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2715329048::INFORMAL"] = "Run {0}: your vote was skipped." + +-- Run {0}: voted on. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T3789682551"] = "Run {0}: voted on." + +-- Run {0}: the models are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T4197671504"] = "Run {0}: the models are done, your vote is missing." + +-- Run {0}: the models are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T4197671504::INFORMAL"] = "Run {0}: the models are done, your vote is missing." + +-- Chat 1 helped more +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2438409751"] = "Chat 1 helped more" + +-- Chat 1 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2553260352"] = "Chat 1" + +-- Chat 2 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2603593209"] = "Chat 2" + +-- Both helped equally +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3093762271"] = "Both helped equally" + +-- Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3870350915"] = "Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat." + +-- Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3870350915::INFORMAL"] = "Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat." + +-- Chat 2 helped more +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T988417422"] = "Chat 2 helped more" + +-- Tasks decided without knowing the models: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1092985245"] = "Tasks decided without knowing the models: {0}" + +-- The judge chose the same as you in {0} of {1} tasks ({2}). +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T118785941"] = "The judge chose the same as you in {0} of {1} tasks ({2})." + +-- The judge chose the same as you in {0} of {1} tasks ({2}). +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T118785941::INFORMAL"] = "The judge chose the same as you in {0} of {1} tasks ({2})." + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1422468824"] = "Your messages on average" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1422468824::INFORMAL"] = "Your messages on average" + +-- After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1709474261"] = "After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models." + +-- After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1709474261::INFORMAL"] = "After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models." + +-- Result of the series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1779013167"] = "Result of the series" + +-- Your effort with each model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1872744172"] = "Your effort with each model" + +-- Your effort with each model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1872744172::INFORMAL"] = "Your effort with each model" + +-- Median time +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2152829620"] = "Median time" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2189814010"] = "Model" + +-- The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2579182820"] = "The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there." + +-- The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2579182820::INFORMAL"] = "The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there." + +-- Decided tasks +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3224295221"] = "Decided tasks" + +-- You decided no task of this series without knowing the models, so it has no result. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3510794091"] = "You decided no task of this series without knowing the models, so it has no result." + +-- You decided no task of this series without knowing the models, so it has no result. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3510794091::INFORMAL"] = "You decided no task of this series without knowing the models, so it has no result." + +-- classic RAG +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1197693637"] = "classic RAG" + +-- Profile: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1311539038"] = "Profile: {0}" + +-- Data sources: {0} ({1}) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1413816655"] = "Data sources: {0} ({1})" + +-- No profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2028602035"] = "No profile" + +-- No data sources +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2502582508"] = "No data sources" + +-- No tools +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2868032641"] = "No tools" + +-- Profile: no longer available +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3128799223"] = "Profile: no longer available" + +-- {0} no longer available +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3209274018"] = "{0} no longer available" + +-- Tools: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3940425864"] = "Tools: {0}" + +-- Semantic Search +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T853993291"] = "Semantic Search" + +-- After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T1606451454"] = "After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task." + +-- After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T1606451454::INFORMAL"] = "After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task." + +-- Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T2709626367"] = "Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them." + +-- Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T2709626367::INFORMAL"] = "Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them." + +-- Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T326613437"] = "Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series." + +-- Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T326613437::INFORMAL"] = "Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series." + +-- A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T49281341"] = "A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind." + +-- A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T49281341::INFORMAL"] = "A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind." + +-- A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T863629402"] = "A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series." + +-- A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T863629402::INFORMAL"] = "A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series." + +-- The judge has not judged this task yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T126436094"] = "The judge has not judged this task yet." + +-- The judge preferred chat {0}, while you preferred the other one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1611643242"] = "The judge preferred chat {0}, while you preferred the other one." + +-- The judge preferred chat {0}, while you preferred the other one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1611643242::INFORMAL"] = "The judge preferred chat {0}, while you preferred the other one." + +-- Judge on task {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1798480084"] = "Judge on task {0}" + +-- The judge found both chats equally helpful, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1811336012"] = "The judge found both chats equally helpful, while you preferred one of them." + +-- The judge found both chats equally helpful, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1811336012::INFORMAL"] = "The judge found both chats equally helpful, while you preferred one of them." + +-- The judge reads both chats. This can take a while. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T185857423"] = "The judge reads both chats. This can take a while." + +-- The judge preferred chat {0}, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T2805496490"] = "The judge preferred chat {0}, as you did." + +-- The judge preferred chat {0}, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T2805496490::INFORMAL"] = "The judge preferred chat {0}, as you did." + +-- Ask the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3611576738"] = "Ask the judge" + +-- The judge preferred chat {0}, while you found both chats equally helpful. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3944557605"] = "The judge preferred chat {0}, while you found both chats equally helpful." + +-- The judge preferred chat {0}, while you found both chats equally helpful. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3944557605::INFORMAL"] = "The judge preferred chat {0}, while you found both chats equally helpful." + +-- Both chats together are too long for the judge, so it did not judge this task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4003597607"] = "Both chats together are too long for the judge, so it did not judge this task." + +-- The judge is no longer available as it was when the series began, so it did not judge this task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4173102270"] = "The judge is no longer available as it was when the series began, so it did not judge this task." + +-- The judge did not give a usable verdict. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4278818434"] = "The judge did not give a usable verdict." + +-- The judge found both chats equally helpful, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T970331360"] = "The judge found both chats equally helpful, as you did." + +-- The judge found both chats equally helpful, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T970331360::INFORMAL"] = "The judge found both chats equally helpful, as you did." + +-- Show the result +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1015029476"] = "Show the result" + +-- Test another task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1029988487"] = "Test another task" + +-- Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1119857700"] = "Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more." + +-- Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1119857700::INFORMAL"] = "Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more." + +-- One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T116035530"] = "One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew." + +-- One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T116035530::INFORMAL"] = "One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew." + +-- You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1163964876"] = "You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway?" + +-- You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1163964876::INFORMAL"] = "You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway?" + +-- Task {0}: chat 1 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T134264261"] = "Task {0}: chat 1" + +-- Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T174840209"] = "Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it." + +-- Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T174840209::INFORMAL"] = "Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it." + +-- AI Studio could not save the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1811212186"] = "AI Studio could not save the comparison series. Please try again." + +-- AI Studio could not save the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1811212186::INFORMAL"] = "AI Studio could not save the comparison series. Please try again." + +-- How a task works +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2064032605"] = "How a task works" + +-- Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2374282918"] = "Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first." + +-- Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2374282918::INFORMAL"] = "Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first." + +-- I'm done with this model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2855215253"] = "I'm done with this model" + +-- Discard task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2966310514"] = "Discard task" + +-- Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3114690087"] = "Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone." + +-- Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3114690087::INFORMAL"] = "Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone." + +-- You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3203252119"] = "You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual." + +-- You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3203252119::INFORMAL"] = "You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual." + +-- The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3217444240"] = "The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result?" + +-- The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3217444240::INFORMAL"] = "The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result?" + +-- Decided tasks so far: {0}. Do you want to test another task? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3225301908"] = "Decided tasks so far: {0}. Do you want to test another task?" + +-- Decided tasks so far: {0}. Do you want to test another task? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3225301908::INFORMAL"] = "Decided tasks so far: {0}. Do you want to test another task?" + +-- Task {0}: your decision +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3586181851"] = "Task {0}: your decision" + +-- Task {0}: your decision +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3586181851::INFORMAL"] = "Task {0}: your decision" + +-- Start a task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3692047609"] = "Start a task" + +-- Setup of this series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3798429422"] = "Setup of this series" + +-- Hide the result +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T4133577695"] = "Hide the result" + +-- Task {0}: chat 2 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T83931404"] = "Task {0}: chat 2" + +-- Complete answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T1705377513"] = "Complete answers" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T2189814010"] = "Model" + +-- Until the first token +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T2818188291"] = "Until the first token" + +-- The median time each model took, so that a single slow answer does not distort the picture. Only complete answers count. The times also depend on the length of the answers and on how busy the servers were. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T357780752"] = "The median time each model took, so that a single slow answer does not distort the picture. Only complete answers count. The times also depend on the length of the answers and on how busy the servers were." + +-- Until the complete answer +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T517769867"] = "Until the complete answer" + +-- A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T1958437853"] = "A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run." + +-- A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T1958437853::INFORMAL"] = "A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run." + +-- Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2158946259"] = "Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge." + +-- Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2158946259::INFORMAL"] = "Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge." + +-- The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2347517054"] = "The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own." + +-- The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2347517054::INFORMAL"] = "The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own." + +-- Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T3491184146"] = "Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”." + +-- Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T3491184146::INFORMAL"] = "Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”." + +-- Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T477339319"] = "Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again." + +-- Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T477339319::INFORMAL"] = "Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again." + +-- Answer A +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T1420338340"] = "Answer A" + +-- Answer B +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T1470671197"] = "Answer B" + +-- Which answer is better? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2388547151"] = "Which answer is better?" + +-- Finish +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2670325426"] = "Finish" + +-- Both are equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2723559248"] = "Both are equally good" + +-- Answer A is better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3123852190"] = "Answer A is better" + +-- Skip remaining votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3188579167"] = "Skip remaining votes" + +-- Skip this run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3334593867"] = "Skip this run" + +-- Next run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3533713133"] = "Next run" + +-- At least one of the models did not answer in this run, so there is nothing to compare. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3561467042"] = "At least one of the models did not answer in this run, so there is nothing to compare." + +-- Run {0} of {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3815324322"] = "Run {0} of {1}" + +-- Answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T46578788"] = "Answers" + +-- You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T651204000"] = "You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end." + +-- You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T651204000::INFORMAL"] = "You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end." + +-- Answer B is better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T933724747"] = "Answer B is better" + -- You can enter text, attach one or more documents, or use both. At least one input is required. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required." @@ -6709,6 +7693,24 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T713722424"] = "AI Studio could no -- AI Studio could not store what the AI received separately from your chat. It remains in the chat itself, so you can still read it. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T713722424::INFORMAL"] = "AI Studio could not store what the AI received separately from your chat. It remains in the chat itself, so you can still read it." +-- The selected provider may not see this conversation. Please select another provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1132710328"] = "The selected provider may not see this conversation. Please select another provider." + +-- The selected provider may not see this conversation. Please select another provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1132710328::INFORMAL"] = "The selected provider may not see this conversation. Please select another provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1809312323"] = "Please select a provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1809312323::INFORMAL"] = "Please select a provider." + +-- AI Studio could not save this conversation. Please try again. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T463511884"] = "AI Studio could not save this conversation. Please try again." + +-- AI Studio could not save this conversation. Please try again. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T463511884::INFORMAL"] = "AI Studio could not save this conversation. Please try again." + -- The local image file does not exist. Skipping the image. UI_TEXT_CONTENT["AISTUDIO::CHAT::IMAGESOURCEEXTENSIONS::T255679918"] = "The local image file does not exist. Skipping the image." @@ -7546,6 +8548,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T346886000"] = "Plugins of y -- Plugins of your organization. Your organization brings them back. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T346886000::INFORMAL"] = "Plugins of your organization. Your organization brings them back." +-- Your presets, results, and comparison series with their chats in the Model Comparison assistant. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3574579770"] = "Your presets, results, and comparison series with their chats in the Model Comparison assistant." + +-- Your presets, results, and comparison series with their chats in the Model Comparison assistant. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3574579770::INFORMAL"] = "Your presets, results, and comparison series with their chats in the Model Comparison assistant." + -- AI Studio keeps your data in two folders on this computer. When you back up your computer with a tool of your own, such as [Restic](https://restic.net), [Borg](https://www.borgbackup.org), or [Time Machine](https://support.apple.com/en-us/104984), this shows which of their contents belong in the backup and which it can leave out. Click a path to copy it to the clipboard, or use the folder button to open it in your file manager. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3596708716"] = "AI Studio keeps your data in two folders on this computer. When you back up your computer with a tool of your own, such as [Restic](https://restic.net), [Borg](https://www.borgbackup.org), or [Time Machine](https://support.apple.com/en-us/104984), this shows which of their contents belong in the backup and which it can leave out. Click a path to copy it to the clipboard, or use the folder button to open it in your file manager." @@ -7930,6 +8938,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T147584079"] = "You -- You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T147584079::INFORMAL"] = "You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message." +-- One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1491430062"] = "One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + +-- One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1491430062::INFORMAL"] = "One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + -- Data Source Selection UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T15302104"] = "Data Source Selection" @@ -7975,6 +8989,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2382076848"] = "Whe -- When the model of a chat cannot use tools, AI Studio uses classic RAG instead and searches your data sources with every message. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2382076848::INFORMAL"] = "When the model of a chat cannot use tools, AI Studio uses classic RAG instead and searches your data sources with every message." +-- One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2654482616"] = "One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + +-- One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2654482616::INFORMAL"] = "One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + -- Whenever a question calls for it, the AI picks the fitting ones among these data sources itself. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2854302511"] = "Whenever a question calls for it, the AI picks the fitting ones among these data sources itself." @@ -8137,6 +9157,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T244391 -- Change what this tile opens UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T4272203100"] = "Change what this tile opens" +-- Stop +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T1266644741"] = "Stop" + +-- AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T1561705002"] = "AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it." + +-- Export message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T274919936"] = "Export message" + +-- Your message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T3642105949"] = "Your message" + +-- Your message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T3642105949::INFORMAL"] = "Your message" + +-- Send +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T4069397711"] = "Send" + +-- approx. {0} tokens +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T857715435"] = "approx. {0} tokens" + -- Copy the path: {0} UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEACTIONS::T1140628382"] = "Copy the path: {0}" @@ -9832,6 +10873,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T749664565"] = "Tool Selec -- No tool can be selected right now. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T953828816"] = "No tool can be selected right now." +-- This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T966234001"] = "This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}." + -- Select tools UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T998515990"] = "Select tools" @@ -12568,6 +13612,240 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Clos -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Close" +-- Compared +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1058933136"] = "Compared" + +-- Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1167627412"] = "Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone." + +-- Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1167627412::INFORMAL"] = "Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone." + +-- {0}: {1} comparisons with {2} runs. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T119967148"] = "{0}: {1} comparisons with {2} runs." + +-- All results +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1647689906"] = "All results" + +-- Speed +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T174817312"] = "Speed" + +-- Same choice +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1835176194"] = "Same choice" + +-- Delete all comparison series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1848485659"] = "Delete all comparison series" + +-- AI Studio could not delete all comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2092205828"] = "AI Studio could not delete all comparison series. Please try again." + +-- AI Studio could not delete all comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2092205828::INFORMAL"] = "AI Studio could not delete all comparison series. Please try again." + +-- Judge +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2094884932"] = "Judge" + +-- AI Studio could not delete all results. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2104729496"] = "AI Studio could not delete all results. Please try again." + +-- AI Studio could not delete all results. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2104729496::INFORMAL"] = "AI Studio could not delete all results. Please try again." + +-- No two different models were compared with each other yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2439811736"] = "No two different models were compared with each other yet." + +-- You have not voted on two different models yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2482334433"] = "You have not voted on two different models yet." + +-- You have not voted on two different models yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2482334433::INFORMAL"] = "You have not voted on two different models yet." + +-- No judge has given a usable verdict yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T265789464"] = "No judge has given a usable verdict yet." + +-- How often each model won by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T268255753"] = "How often each model won by the verdicts of the judges." + +-- Agreement +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2750704103"] = "Agreement" + +-- How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2888156258"] = "How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers." + +-- How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2888156258::INFORMAL"] = "How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers." + +-- Ranking by the judges +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3071310071"] = "Ranking by the judges" + +-- Agreement with your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3243788701"] = "Agreement with your votes" + +-- Agreement with your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3243788701::INFORMAL"] = "Agreement with your votes" + +-- Judges +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3347779221"] = "Judges" + +-- {0}: {1} comparison series with {2} tasks decided without knowing the models. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3355735368"] = "{0}: {1} comparison series with {2} tasks decided without knowing the models." + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3448155331"] = "Close" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T360017507"] = "Your votes" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T360017507::INFORMAL"] = "Your votes" + +-- There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3704633531"] = "There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”." + +-- There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3704633531::INFORMAL"] = "There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”." + +-- How often each of two models won when they competed directly, by your votes and by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T372027252"] = "How often each of two models won when they competed directly, by your votes and by the verdicts of the judges." + +-- How often each of two models won when they competed directly, by your votes and by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T372027252::INFORMAL"] = "How often each of two models won when they competed directly, by your votes and by the verdicts of the judges." + +-- How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3831905913"] = "How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count." + +-- How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3831905913::INFORMAL"] = "How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count." + +-- Delete all results and comparison series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3967608788"] = "Delete all results and comparison series" + +-- Delete the results of “Answer Variance” +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T4059958648"] = "Delete the results of “Answer Variance”" + +-- Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T726138079"] = "Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone." + +-- Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T726138079::INFORMAL"] = "Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone." + +-- Pairs +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T865427974"] = "Pairs" + +-- Only the mode “Answer Variance” measures how fast the models answer. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T922668471"] = "Only the mode “Answer Variance” measures how fast the models answer." + +-- Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T93821586"] = "Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone." + +-- Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T93821586::INFORMAL"] = "Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone." + +-- Judge (optional) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1206060603"] = "Judge (optional)" + +-- This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T134968648"] = "This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series." + +-- This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T134968648::INFORMAL"] = "This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series." + +-- Please choose two different models. A series compares how well two models help you. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1531478547"] = "Please choose two different models. A series compares how well two models help you." + +-- Please choose two different models. A series compares how well two models help you. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1531478547::INFORMAL"] = "Please choose two different models. A series compares how well two models help you." + +-- Create series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2037222750"] = "Create series" + +-- Name of the series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2083243781"] = "Name of the series" + +-- No judge +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2562272449"] = "No judge" + +-- Name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T266367750"] = "Name" + +-- The profile no longer exists, so the series starts without a profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2782528640"] = "The profile no longer exists, so the series starts without a profile." + +-- The profile no longer exists, so the series starts without a profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2782528640::INFORMAL"] = "The profile no longer exists, so the series starts without a profile." + +-- Models +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T30216107"] = "Models" + +-- The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3472943722"] = "The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3472943722::INFORMAL"] = "The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- Ask a judge? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3641185871"] = "Ask a judge?" + +-- Tools: {0}, data sources: {1} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3667830435"] = "Tools: {0}, data sources: {1}" + +-- The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3712631176"] = "The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3712631176::INFORMAL"] = "The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- AI Studio could not save the new comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3741421828"] = "AI Studio could not save the new comparison series. Please try again." + +-- AI Studio could not save the new comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3741421828::INFORMAL"] = "AI Studio could not save the new comparison series. Please try again." + +-- Profile, tools, and data sources +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3936393556"] = "Profile, tools, and data sources" + +-- Choose both models first: what you can select here depends on them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4030857378"] = "Choose both models first: what you can select here depends on them." + +-- Choose both models first: what you can select here depends on them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4030857378::INFORMAL"] = "Choose both models first: what you can select here depends on them." + +-- First model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4044223184"] = "First model" + +-- A judge assesses every task +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4111877217"] = "A judge assesses every task" + +-- Some data sources are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T511469979"] = "Some data sources are no longer available, so the series starts without them." + +-- Some data sources are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T511469979::INFORMAL"] = "Some data sources are no longer available, so the series starts without them." + +-- Some tools are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T600304160"] = "Some tools are no longer available, so the series starts without them." + +-- Some tools are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T600304160::INFORMAL"] = "Some tools are no longer available, so the series starts without them." + +-- e.g., Weekly reports +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T795365587"] = "e.g., Weekly reports" + +-- The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T805855292"] = "The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off." + +-- The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T805855292::INFORMAL"] = "The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off." + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T867467332"] = "Second model" + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T900713019"] = "Cancel" + +-- Judge model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T953975257"] = "Judge model" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." @@ -14575,6 +15853,66 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4049873 -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Preselect the web content reader?" +-- No preset preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1028082569"] = "No preset preselected" + +-- Preselected preset +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1094542630"] = "Preselected preset" + +-- The mode “Answer Variance” opens with this preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1310335465"] = "The mode “Answer Variance” opens with this preset." + +-- No Model Comparison options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1495085044"] = "No Model Comparison options are preselected" + +-- Model Comparison options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2330752759"] = "Model Comparison options are preselected" + +-- Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2350726718"] = "Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model." + +-- Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2350726718::INFORMAL"] = "Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model." + +-- The preselected preset no longer exists. Please select another preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2696863075"] = "The preselected preset no longer exists. Please select another preset." + +-- The preselected preset no longer exists. Please select another preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2696863075::INFORMAL"] = "The preselected preset no longer exists. Please select another preset." + +-- No model preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2802610587"] = "No model preselected" + +-- When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2982794040"] = "When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against." + +-- When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2982794040::INFORMAL"] = "When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against." + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3448155331"] = "Close" + +-- Preselect Model Comparison options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3640087463"] = "Preselect Model Comparison options?" + +-- Preselected judge model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3746033265"] = "Preselected judge model" + +-- A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3884208586"] = "A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on." + +-- A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3884208586::INFORMAL"] = "A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on." + +-- Missing preset ({0}) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T413828499"] = "Missing preset ({0})" + +-- Assistant: Model Comparison Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T4212945869"] = "Assistant: Model Comparison Options" + +-- Preselected model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T473925378"] = "Preselected model" + -- Choose whether the assistant should use the app default profile, no profile, or a specific profile. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." @@ -15967,6 +17305,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1777666968"] = "Prompt Optimizer" -- SWOT Analysis UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1864542518"] = "SWOT Analysis" +-- Compare two models in a blind test for how much their answers vary and how useful they are. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1869142375"] = "Compare two models in a blind test for how much their answers vary and how useful they are." + +-- Compare two models in a blind test for how much their answers vary and how useful they are. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1869142375::INFORMAL"] = "Compare two models in a blind test for how much their answers vary and how useful they are." + -- Slide Assistant UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Slide Assistant" @@ -16081,6 +17425,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3881504200"] = "Use an LLM to fin -- Use an LLM to find an icon for a given context. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3881504200::INFORMAL"] = "Use an LLM to find an icon for a given context." +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3910956815"] = "Model Comparison" + -- Job Posting UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3930052338"] = "Job Posting" @@ -17785,6 +19132,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It -- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511::INFORMAL"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again." +-- hidden +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::PROVIDERIDENTITYEXTENSIONS::T4128829753"] = "hidden" + -- Text too long UI_TEXT_CONTENT["AISTUDIO::PROVIDER::PROVIDERREQUESTFAILUREREASONEXTENSIONS::T1074711534"] = "Text too long" @@ -18478,6 +19828,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T555062689"] = "Log View -- New Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "New Chat" +-- Model Comparison Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T941908687"] = "Model Comparison Assistant" + -- Trust LLM providers from the USA UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T1748300640"] = "Trust LLM providers from the USA" diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor new file mode 100644 index 000000000..7451f2d13 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor @@ -0,0 +1,149 @@ +@attribute [Route(Routes.ASSISTANT_MODEL_COMPARISON_STABILITY)] +@inherits AssistantBaseCore + +@* Two views: the form to fill in, and the batch in short while it runs and once it has. *@ +@if (!this.ShowsBatch) +{ + + @T("Presets") + + + + @TA("A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself.") + + + + @foreach (var preset in this.presets) + { + + @this.ListedName(preset) + + } + + + + + @T("Add preset") + + + @T("Delete this preset") + + + + + + @if (this.SelectedPreset is not null) + { + + } + + @T("Models") + + @ModelComparisonTexts.WhatTheModelsAreFor + + + + @T("First model") + + + + @T("Second model") + + + + + @T("Number of runs") + + @ModelComparisonTexts.WhatTheRunsAreFor + + + @T("1 (single comparison)") + 3 + 5 + 10 + 15 + @T("20 (heavy load on the models)") + + + @TA("Your request") + + @ModelComparisonTexts.WhatTheModelsReceive + + + + @T("Material (optional)") + + @ModelComparisonTexts.WhatTheMaterialIs + + + + + @* The form checks the fields of the judge also while the panel is collapsed: *@ + + + @ModelComparisonTexts.WhatTheJudgeIsFor + + + @if (this.judgeEnabled) + { + @T("Judge model") + + + + } + +} +else +{ + @* What was asked, in short: without it, the answers below would have to be judged from memory. *@ + var request = this.CurrentRequest; + + + + + @TA("Your request") + + + + + + + @request.GetQuestionPreview() + + @if (request.Context.Length > 0) + { + + @string.Format(DisplayCulture, T("Material: {0} characters"), request.Context.Length.CompactCount()) + + } + + + @if (this.batch is { RunCount: > 1 } shownBatch) + { + + } + +} + +@code { + + private protected override RenderFragment HeaderActions => @ + + + + + + + ; + + private protected override RenderFragment BelowSubmitContent => @; + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor.cs new file mode 100644 index 000000000..6d451a2fd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparison.razor.cs @@ -0,0 +1,850 @@ +using AIStudio.Dialogs; +using AIStudio.Dialogs.Settings; +using AIStudio.Tools.AssistantSessions; +using AIStudio.Tools.Security; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Assistants.ModelComparison; + +public partial class AssistantModelComparison : AssistantBaseCore +{ + [Inject] + private IDialogService DialogService { get; init; } = null!; + + [Inject] + private PromptInjectionGuardService PromptInjectionGuard { get; init; } = null!; + + [Inject] + private ModelComparisonStore ModelComparisonStore { get; init; } = null!; + + protected override Tools.Components Component => Tools.Components.MODEL_COMPARISON_ASSISTANT; + + protected override string Title => ModelComparisonMode.STABILITY.PageTitle; + + protected override string Description => TA("Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer."); + + protected override string SystemPrompt => string.Empty; + + // The answers of this assistant are shown and judged on its own page, so no chat continues its conversations: + protected override ChatSystemPromptChoice ChatSystemPrompt => ChatSystemPromptChoice.AI_STUDIO_DEFAULT; + + protected override bool AllowProfiles => false; + + protected override bool ShowResult => false; + + /// + /// The footer of the base class offers to send the result to another assistant, and to copy it. + /// Both work on the one result the base class knows about, and this assistant has none: its + /// answers are shown by the page itself. Each answer has a copy button of its own. + /// + protected override bool ShowSendTo => false; + + protected override bool ShowCopyResult => false; + + /// + /// Two models and a judge: the footer would show the confidence of the first model only. Each model selection + /// shows the confidence of its own model instead. + /// + protected override bool ShowProviderConfidence => false; + + protected override string SubmitText => T("Compare"); + + /// + /// Once there are results, the request is not editable, so there is nothing to submit. The way + /// to the next comparison is the button below the summary. + /// + /// + /// The button of the base class cannot be hidden, and repurposing it would start a session of + /// the base class just to switch the view, with the waiting animation flashing by. + /// + protected override bool SubmitDisabled => this.ShowsBatch; + + protected override Func SubmitAction => this.Compare; + + /// + /// Starts over with the selected preset as it is stored: OnDefaultsAppliedAsync fills the form with it right after + /// this ran. + /// + protected override void ResetForm() + { + this.hasAttemptedCompare = false; + this.isComparing = false; + this.ClearBatch(); + } + + /// + /// How many independent runs to make of the same comparison. One is the plain comparison this + /// assistant always offered; more repeat the same request several times, to see how consistently + /// the two models -- and the judge -- come out the same way. + /// + private int runCount = 1; + + /// + /// The batch which runs or ran, or null while the form is shown. + /// + private ModelComparisonBatch? batch; + + /// + /// Whether the batch is being asked right now: at least one run is still in flight. Set once the + /// form has been validated, and not when the session starts: the base class marks the session as + /// processing before the request has been checked, and the form must not be taken away before + /// that. + /// + private bool isComparing; + + /// + /// Whether the page shows the batch instead of the form: while it runs, and once runs arrived. + /// + private bool ShowsBatch => this.isComparing || this.batch is { Entries.Count: > 0 }; + + /// + /// A render already queued on the UI dispatcher, if one is still pending. Reused by RequestBackgroundUIRefresh, so + /// a burst of runs arriving close together queues at most one more render instead of one per run. + /// + private Task? pendingBackgroundRefresh; + + /// + /// Asks for a UI refresh the same way RefreshAssistantUIAsync does, but skips + /// asking again while one is already queued. + /// + /// + /// Blazor Server processes every queued render, and every click, on the same one dispatcher queue, each in its own + /// turn. With up to ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS runs arriving close together, a render per run + /// queues up several of them ahead of whatever the user just clicked -- the vote button above all -- which is + /// exactly the multi-second stall this avoids. A run which arrives while a render is already queued does not need + /// one of its own: the queued render still runs after this run was added to the batch, so it shows it all the same. + /// + private void RequestBackgroundUIRefresh() + { + if (this.pendingBackgroundRefresh is { IsCompleted: false }) + return; + + this.pendingBackgroundRefresh = this.RefreshAssistantUIAsync(); + } + + /// + /// Forgets the current batch, which brings the form back. + /// + private void ClearBatch() => this.batch = null; + + private string inputContext = string.Empty; + private string inputQuestion = string.Empty; + + /// + /// What both models are asked, as the user entered it right now. Both models, and the judge with + /// them, are asked to answer in the app's active language, regardless of what language the + /// question itself is written in. + /// + private ModelComparisonRequest CurrentRequest => new(this.inputContext, this.inputQuestion, ModelComparisonFormat.LanguageName); + + protected override bool MightPreselectValues() => false; + + /// + /// Fills the form with the selected preset, once the page opens and after every reset. The page always works in a + /// preset: without any, it adds one. + /// + /// + /// The base class preselects the first model through GetPreselectedProvider by then, which knows nothing of the + /// presets. An assistant session which the base class restores afterward brings back its own form and preset, see + /// RestoreCustomAssistantSessionState. + /// + protected override async Task OnDefaultsAppliedAsync() + { + if (this.SelectedPreset is null) + this.selectedPresetId = ModelComparisonPresetChoice.Choose(this.presets, this.SettingsManager.ConfigurationData.ModelComparison) ?? await this.AddNewPreset(); + + this.LoadSelectedPreset(); + } + + /// + /// The first model is ProviderSettings of the base class, so that everything the + /// base class does with a provider keeps working for it. This is the second one. + /// + private AIStudio.Settings.Provider secondProvider = Settings.Provider.NONE; + + /// + /// Whether an optional third model judges the two answers. On from the start only when the options of the + /// assistant preselect a judge model, see OnDefaultsAppliedAsync. + /// + private bool judgeEnabled; + + /// + /// Whether the panel of the judge is open. A preset with a judge opens it, so that the user sees the judge at once. + /// + private bool judgePanelExpanded; + + /// + /// The judge model, asked only when judgeEnabled is set. + /// + private AIStudio.Settings.Provider judgeProvider = Settings.Provider.NONE; + + /// + /// The expectations, values, and goals of the user, in their own words, by which the judge decides which answer is + /// better, see ModelComparisonJudgeExpectations. Sent to the judge together with the request and both answers, but + /// the judge never learns which model wrote which answer. + /// + private string judgeInstructions = string.Empty; + + /// + /// Whether the user confirmed that the judge may decide by its own standards, which counts only while + /// judgeInstructions is empty. Not part of a preset: whoever describes nothing confirms that once per session. + /// + private bool judgeOwnStandardsAccepted; + + /// + /// Whether the user has tried to submit the form at least once. The judge's provider field mounts + /// only once the judge is switched on, which can happen well after the form itself first + /// rendered, and MudForm would otherwise mark it invalid on sight, before the user had any chance + /// to pick one. ValidatingJudgeProvider stays silent until this is set, regardless + /// of when MudForm itself happens to call it. + /// + private bool hasAttemptedCompare; + + /// + /// Whether the summary of the request (the question preview and the context's length) is + /// expanded, while a batch is shown. Collapsed by default: useful to check back on, not + /// something to stare at while a batch runs. + /// + private bool requestSummaryExpanded; + + /// + /// The presets as they are stored, sorted by name when the store listed them last. + /// + private List presets = []; + + /// + /// The ID of the preset the form shows and changes, or null when the store could not provide any. + /// + private Guid? selectedPresetId; + + /// + /// What the name field shows for the selected preset, which may be a name not valid yet. + /// + private string presetName = string.Empty; + + /// + /// The preset selectedPresetId points to, or null when there is none. + /// + private ModelComparisonPreset? SelectedPreset => this.presets.FirstOrDefault(preset => preset.Id == this.selectedPresetId); + + /// + /// Whether the presets may change right now: not while the models are being asked. + /// + private bool ArePresetControlsDisabled => this.IsProcessing; + + protected override async Task OnInitializedAsync() + { + // The presets come first: the base class fills the form with one of them, see OnDefaultsAppliedAsync. + await this.ReloadPresets(); + await base.OnInitializedAsync(); + } + + /// + /// Stores a change of the form, a moment after the user stopped typing. + /// + protected override async Task OnFormChange() => await this.StorePresetIfChanged(); + + /// + /// Stores what the form changed, also when the user leaves the page before the form noticed the change. + /// + /// + /// The store serves the whole app, so it finishes this even after the page is gone. A newer change of the same + /// preset, e.g., after the user came back right away, is stored after it, since the store writes each preset one + /// after the other. + /// + protected override void DisposeResources() + { + this.StorePresetIfChanged().Observe("Model Comparison: storing the preset when leaving the page"); + base.DisposeResources(); + } + + /// + /// Lists the presets anew. + /// + private async Task ReloadPresets() + { + try + { + this.presets = [.. await this.ModelComparisonStore.ListPresetsAsync()]; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogWarning(exception, "Could not list the presets of the Model Comparison assistant."); + } + } + + /// + /// Stores a change of the presets. + /// + /// The change: a save or a delete in the store. + /// Whether the change was stored. + private async Task ChangePresets(Func change) + { + try + { + await change(); + return true; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not store a change of the presets of the Model Comparison assistant."); + await MessageBus.INSTANCE.SendError(new(AppIcons.ASSISTANT_MODEL_COMPARISON, TA("AI Studio could not save the change to your presets. Please try again."))); + return false; + } + } + + /// + /// Captures the form as it stands right now as a preset. + /// + /// The ID of the preset. + /// The name of the preset. + /// The preset. + private ModelComparisonPreset PresetFromForm(Guid id, string name) => new() + { + Id = id, + Name = name, + FirstProviderId = this.ProviderSettings.Id, + SecondProviderId = this.secondProvider.Id, + JudgeEnabled = this.judgeEnabled, + JudgeProviderId = this.judgeProvider.Id, + JudgeInstructions = this.judgeInstructions, + Context = this.inputContext, + Question = this.inputQuestion, + RunCount = this.runCount, + }; + + /// + /// Fills the form with the selected preset. + /// + private void LoadSelectedPreset() + { + var preset = this.SelectedPreset; + if (preset is null) + { + this.presetName = string.Empty; + return; + } + + this.ProviderSettings = this.SettingsManager.GetProviderById(preset.FirstProviderId); + this.secondProvider = this.SettingsManager.GetProviderById(preset.SecondProviderId); + this.judgeEnabled = preset.JudgeEnabled; + this.judgePanelExpanded = preset.JudgeEnabled; + this.judgeProvider = this.SettingsManager.GetProviderById(preset.JudgeProviderId); + this.judgeInstructions = preset.JudgeInstructions; + this.inputContext = preset.Context; + this.inputQuestion = preset.Question; + this.runCount = preset.RunCount; + this.presetName = preset.Name; + } + + /// + /// Stores the form into the selected preset, when it changed anything about it. + /// + /// + /// A name which is not valid yet, e.g., an empty one while the user retypes it, is not stored: the preset keeps + /// the name it was stored with until the new one is valid. + /// + private async Task StorePresetIfChanged() + { + if (this.SelectedPreset is not { } storedPreset) + return; + + var name = this.ValidatingPresetName(this.presetName) is null ? this.presetName.Trim() : storedPreset.Name; + var changedPreset = this.PresetFromForm(storedPreset.Id, name); + if (changedPreset == storedPreset) + return; + + if (!await this.ChangePresets(() => this.ModelComparisonStore.SavePresetAsync(changedPreset))) + return; + + // Only now, so that a store which failed is tried again with the next change. The list may have been listed anew meanwhile: + var index = this.presets.FindIndex(preset => preset.Id == changedPreset.Id); + if (index >= 0) + this.presets[index] = changedPreset; + } + + /// + /// Switches the form to another preset, once the changes of the current one are stored. + /// + /// The ID of the preset. + private async Task SelectedPresetChanged(Guid presetId) + { + if (this.ArePresetControlsDisabled || presetId == this.selectedPresetId || this.presets.All(preset => preset.Id != presetId)) + return; + + await this.StorePresetIfChanged(); + await this.SelectPreset(presetId); + } + + /// + /// Fills the form with a preset and forgets the complaints about the previous one. + /// + /// The ID of the preset. + private async Task SelectPreset(Guid? presetId) + { + this.selectedPresetId = presetId; + this.LoadSelectedPreset(); + this.hasAttemptedCompare = false; + if (this.Form is not null) + await this.Form.ResetValidationAsync(); + + this.ClearInputIssues(); + } + + /// + /// Adds a preset and switches the form to it, once the changes of the current one are stored. + /// + private async Task AddPreset() + { + if (this.ArePresetControlsDisabled) + return; + + await this.StorePresetIfChanged(); + if (await this.AddNewPreset() is { } presetId) + await this.SelectPreset(presetId); + } + + /// + /// Stores a new preset with the models which the options of the assistant preselect, and lists the presets anew. + /// + /// The ID of the new preset, or null when it could not be stored. + private async Task AddNewPreset() + { + var judge = this.SettingsManager.PreselectedComparisonJudge; + var preset = new ModelComparisonPreset + { + Id = Guid.NewGuid(), + + // With seconds, so that two presets added right after each other do not get the same name, which has to be unique: + Name = string.Format(DisplayCulture, T("Preset from {0}"), DateTime.Now), + FirstProviderId = this.SettingsManager.PreselectedComparisonModel.Id, + JudgeEnabled = judge != Settings.Provider.NONE, + JudgeProviderId = judge.Id, + }; + + if (!await this.ChangePresets(() => this.ModelComparisonStore.SavePresetAsync(preset))) + return null; + + await this.ReloadPresets(); + return preset.Id; + } + + /// + /// Gets the name the list shows for a preset: for the selected one, the name the user is typing, so that the list + /// never shows an outdated name. The preset itself is stored a moment later, see StorePresetIfChanged. + /// + /// The preset. + private string ListedName(ModelComparisonPreset preset) => preset.Id == this.selectedPresetId && !string.IsNullOrWhiteSpace(this.presetName) + ? this.presetName.Trim() + : preset.Name; + + /// + /// Deletes the selected preset, after asking: a preset can carry a long material text that took real effort to + /// assemble, and one click by mistake would lose it for good. The form then switches to another preset. + /// + private async Task DeletePreset() + { + if (this.ArePresetControlsDisabled || this.SelectedPreset is not { } preset) + return; + + var parameters = new DialogParameters + { + { dialog => dialog.Message, string.Format(DisplayCulture, TA("Are you sure you want to delete the preset '{0}'? This cannot be undone."), preset.Name) }, + }; + + var reference = await this.DialogService.ShowAsync(T("Delete preset"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled) + return; + + if (!await this.ChangePresets(() => this.ModelComparisonStore.DeletePresetAsync(preset.Id))) + return; + + await this.ReloadPresets(); + await this.SelectPreset(ModelComparisonPresetChoice.Choose(this.presets, this.SettingsManager.ConfigurationData.ModelComparison) ?? await this.AddNewPreset()); + } + + /// + /// Checks the name of the selected preset, the same way the Document Analysis checks the names of its policies. + /// + private string? ValidatingPresetName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return TA("Please provide a name for your preset."); + + var trimmedName = name.Trim(); + if (trimmedName.Length is > 60 or < 6) + return TA("The name of your preset must be between 6 and 60 characters long."); + + if (ModelComparisonNames.IsTaken(trimmedName, this.presets.Where(preset => preset.Id != this.selectedPresetId).Select(preset => preset.Name))) + return TA("A preset with this name already exists. Please choose a different name."); + + return null; + } + + private static readonly AssistantSessionStateKey INPUT_CONTEXT_STATE_KEY = new(nameof(inputContext)); + private static readonly AssistantSessionStateKey INPUT_QUESTION_STATE_KEY = new(nameof(inputQuestion)); + private static readonly AssistantSessionStateKey IS_COMPARING_STATE_KEY = new(nameof(isComparing)); + private static readonly AssistantSessionStateKey RUN_COUNT_STATE_KEY = new(nameof(runCount)); + private static readonly AssistantSessionStateKey BATCH_STATE_KEY = new(nameof(batch)); + private static readonly AssistantSessionStateKey SECOND_PROVIDER_STATE_KEY = new(nameof(secondProvider)); + private static readonly AssistantSessionStateKey JUDGE_ENABLED_STATE_KEY = new(nameof(judgeEnabled)); + private static readonly AssistantSessionStateKey JUDGE_PROVIDER_STATE_KEY = new(nameof(judgeProvider)); + private static readonly AssistantSessionStateKey JUDGE_INSTRUCTIONS_STATE_KEY = new(nameof(judgeInstructions)); + private static readonly AssistantSessionStateKey JUDGE_OWN_STANDARDS_ACCEPTED_STATE_KEY = new(nameof(judgeOwnStandardsAccepted)); + private static readonly AssistantSessionStateKey SELECTED_PRESET_ID_STATE_KEY = new(nameof(selectedPresetId)); + private static readonly AssistantSessionStateKey PRESET_NAME_STATE_KEY = new(nameof(presetName)); + + /// + protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) + { + state.Set(INPUT_CONTEXT_STATE_KEY, this.inputContext); + state.Set(INPUT_QUESTION_STATE_KEY, this.inputQuestion); + state.Set(IS_COMPARING_STATE_KEY, this.isComparing); + state.Set(RUN_COUNT_STATE_KEY, this.runCount); + state.Set(BATCH_STATE_KEY, this.batch?.Copy()); + state.Set(SECOND_PROVIDER_STATE_KEY, this.secondProvider); + state.Set(JUDGE_ENABLED_STATE_KEY, this.judgeEnabled); + state.Set(JUDGE_PROVIDER_STATE_KEY, this.judgeProvider); + state.Set(JUDGE_INSTRUCTIONS_STATE_KEY, this.judgeInstructions); + state.Set(JUDGE_OWN_STANDARDS_ACCEPTED_STATE_KEY, this.judgeOwnStandardsAccepted); + state.Set(SELECTED_PRESET_ID_STATE_KEY, this.selectedPresetId); + state.Set(PRESET_NAME_STATE_KEY, this.presetName); + } + + /// + /// + /// The restored form belongs to the preset of the session, which therefore wins over the one OnDefaultsAppliedAsync + /// chose: otherwise, the next change would store this form into another preset. When the preset of the session was + /// deleted meanwhile, the form shows the chosen preset instead, unless the session holds a batch: its results belong + /// to the form of the session. The preset list is not shown along with a batch, so this does not happen through + /// this page. The base class restores the first model before this method, so loading the preset here wins over it. + /// + protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state) + { + state.Restore(INPUT_CONTEXT_STATE_KEY, value => this.inputContext = value); + state.Restore(INPUT_QUESTION_STATE_KEY, value => this.inputQuestion = value); + state.Restore(IS_COMPARING_STATE_KEY, value => this.isComparing = value); + state.Restore(RUN_COUNT_STATE_KEY, value => this.runCount = value); + state.Restore(BATCH_STATE_KEY, value => this.batch = value?.Copy()); + state.Restore(SECOND_PROVIDER_STATE_KEY, value => this.secondProvider = value); + state.Restore(JUDGE_ENABLED_STATE_KEY, value => this.judgeEnabled = value); + state.Restore(JUDGE_PROVIDER_STATE_KEY, value => this.judgeProvider = value); + state.Restore(JUDGE_INSTRUCTIONS_STATE_KEY, value => this.judgeInstructions = value); + state.Restore(JUDGE_OWN_STANDARDS_ACCEPTED_STATE_KEY, value => this.judgeOwnStandardsAccepted = value); + + Guid? sessionPresetId = null; + state.Restore(SELECTED_PRESET_ID_STATE_KEY, value => sessionPresetId = value); + if (sessionPresetId is { } presetId && this.presets.Any(preset => preset.Id == presetId)) + { + this.selectedPresetId = presetId; + state.Restore(PRESET_NAME_STATE_KEY, value => this.presetName = value); + } + else if (this.batch is null) + this.LoadSelectedPreset(); + } + + /// + /// Starts every run of the batch and shows each one for the vote as soon as it is ready. + /// + /// + /// The base class has already started the session, so the token of the stop button is the one which ends every run + /// still in flight. Cancelling it needs no special handling here: every run, in flight or still queued behind + /// ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS, resolves into an entry to show either way -- the batch runner + /// never lets a run end any other way -- so the loop below simply keeps consuming them until none are left. + /// + private async Task Compare() + { + // + // Set before validating, so the judge provider field -- silent about being empty until now -- + // reports a real error on this very attempt, the same as every other required field does: + // + this.hasAttemptedCompare = true; + + // + // The validation has to come first, while the fields of the form are still on the screen: it + // checks the fields which are mounted. A form which has been taken away has nothing left to + // find fault with, and an empty question would go through. + // + await this.Form!.ValidateAsync(); + + // + // The fields of the judge sit in a panel the user may have closed. The form checks them all the same, but the + // user would not see which one is wrong. So the page opens the panel: + // + if (this.JudgeHasIssue) + this.judgePanelExpanded = true; + + if (!this.InputIsValid) + return; + + // The preset keeps what was compared, also when the form changed only a moment ago: + await this.StorePresetIfChanged(); + + this.ClearBatch(); + this.batch = new ModelComparisonBatch(this.runCount); + + // From here on, the request is shown as a summary instead of the form: + this.isComparing = true; + await this.CheckpointAssistantSession(); + await this.RefreshAssistantUIAsync(); + + var token = this.CancellationTokenSource?.Token ?? CancellationToken.None; + + try + { + // One action for the whole batch: the user hears once, when it is over, what the filter removed from the answers the judge read: + await using var promptInjectionScope = this.PromptInjectionGuard.BeginAction(); + + var judgeInputFilter = ModelComparisonProviderAdapter.CreateJudgeInputFilter(this.PromptInjectionGuard, promptInjectionScope, this.Title); + var batchRunner = new ModelComparisonBatchRunner(new ModelComparisonRunner(this.Logger, judgeInputFilter)); + var judge = this.judgeEnabled + ? ModelComparisonProviderAdapter.CreateParticipant(this.judgeProvider, this.SettingsManager, this.Component, this.Title) + : null; + + var tasks = batchRunner.Start( + ModelComparisonProviderAdapter.CreateParticipant(this.ProviderSettings, this.SettingsManager, this.Component, this.Title), + ModelComparisonProviderAdapter.CreateParticipant(this.secondProvider, this.SettingsManager, this.Component, this.Title), + this.CurrentRequest, + this.runCount, + token, + judge, + this.judgeInstructions); + + // + // ConfigureAwait(false) throughout this loop, deliberately: Blazor Server processes every + // click on this page through the same synchronization context this method would otherwise + // keep resuming on for as long as the batch runs. A click has to wait its turn behind + // whatever is already queued on that context, and a background loop which keeps hopping + // back onto it -- once per run, for a whole batch -- can make that turn a long time coming. + // It touches that context on purpose only twice per run: to add the run to the lists, which + // costs next to nothing, and to render, which is not awaited -- see the remark further down. + // + await foreach (var completedTask in Task.WhenEach(tasks).ConfigureAwait(false)) + { + var entry = await completedTask.ConfigureAwait(false); + + // + // The batch is what the page renders and what a vote changes, so it changes on the + // dispatcher of the page only, never here on the thread pool: it is not safe for a + // writer on one thread and a reader or a second writer on another. + // + await this.InvokeAsync(() => this.batch?.Add(entry)).ConfigureAwait(false); + + // + // Coalesced, not one render per run: every render -- this one included -- queues on + // the same one dispatcher a click on this page has to wait its turn on too. Several + // runs arriving close together, up to ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS of + // them, would otherwise queue up that many renders ahead of whatever the user just + // clicked, which is exactly the multi-second stall this avoids. + // + this.RequestBackgroundUIRefresh(); + } + + // The snapshot reads the batch, so it is taken on the dispatcher as well: + await this.InvokeAsync(this.CheckpointAssistantSession).ConfigureAwait(false); + } + finally + { + await this.InvokeAsync(() => + { + // Whatever ended the batch, a canceled one as well, the form is back unless there are results: + this.isComparing = false; + this.batch?.Settle(token.IsCancellationRequested); + }).ConfigureAwait(false); + + this.RequestBackgroundUIRefresh(); + + // A batch whose remaining votes the user skipped, or which the user stopped, is over only now: + await this.InvokeAsync(this.StoreResultsIfFinished).ConfigureAwait(false); + } + } + + /// + /// Goes back from the results to the request, which is as the user left it. + /// + /// + /// Offered with the summary, once the user is through with every run. Before that, stopping the + /// batch (the button next to its progress) reaches the same screen: whatever has arrived by then + /// stays shown. + /// + private async Task StartNewBatch() + { + this.ClearBatch(); + await this.CheckpointAssistantSession(); + } + + /// + /// Shows what all stored comparisons say about the models. The results of a batch are stored once its + /// summary shows, so the dialog counts them already. + /// + private async Task OpenResultsDialog() => await this.DialogService.ShowAsync(T("Results of all comparisons"), DialogOptions.FULLSCREEN); + + /// + /// Goes back to the choice between the two modes. A batch in progress goes on, and the assistant session brings it + /// back on return. + /// + private void SwitchMode() => this.NavigationManager.NavigateTo(Routes.ASSISTANT_MODEL_COMPARISON); + + /// + /// Takes the vote for the current run and moves on to the next one, or to the summary after the last run. The + /// names of the models stay hidden until the summary, see ModelComparisonBatch.Vote. + /// + private async Task Vote(ModelComparisonVote vote) + { + if (this.batch?.Vote(vote) is not true) + return; + + await this.ShowCurrentEntry(); + } + + /// + /// Moves on past a run which has nothing to vote on, because a model of that run did not answer. + /// + private async Task GoToNextEntry() + { + if (this.batch?.GoToNextEntry() is not true) + return; + + await this.ShowCurrentEntry(); + } + + /// + /// Moves on to the next run without voting on this one. + /// + private async Task SkipThisRun() + { + if (this.batch is null) + return; + + this.batch.SkipThisRun(); + await this.ShowCurrentEntry(); + } + + /// + /// Stops asking for a vote on every remaining run of this batch. A run still resolves itself as it arrives, but + /// none of them are shown individually anymore: the summary appears once the whole batch has settled, which may + /// already be the case right now. + /// + private async Task SkipRemainingVotes() + { + if (this.batch is null) + return; + + this.batch.SkipRemainingVotes(this.isComparing); + await this.ShowCurrentEntry(); + } + + /// + /// Shows the run the batch moved on to, or its summary. + /// + /// + /// The next run is shown before the checkpoint is awaited, not after: a batch snapshot carries the answer text of + /// every run so far, and saving it can take a moment the click must not wait on. + /// + private async Task ShowCurrentEntry() + { + await this.RefreshAssistantUIAsync(); + await this.CheckpointAssistantSession(); + await this.StoreResultsIfFinished(); + } + + /// + /// The batch whose results this page stored already, so that it does not write them again: after the last vote + /// and once more when the batch settles, for example. + /// + private Guid? storedBatchId; + + /// + /// Stores the results of the batch once the user is through with it, for the statistics. + /// + /// + /// Only then all votes are in. A batch without any vote and without any verdict of a judge holds nothing to count + /// and is not stored. Called on the dispatcher of the page only, since it reads the batch. + /// + private async Task StoreResultsIfFinished() + { + if (this.batch is not { IsFinished: true } finishedBatch || this.storedBatchId == finishedBatch.Id) + return; + + var record = ModelComparisonRecord.Of( + finishedBatch, + DateTimeOffset.UtcNow, + this.CurrentRequest.GetQuestionPreview(), + ModelComparisonModelIdentity.Of(this.ProviderSettings), + ModelComparisonModelIdentity.Of(this.secondProvider), + this.judgeEnabled ? ModelComparisonModelIdentity.Of(this.judgeProvider) : null); + + this.storedBatchId = finishedBatch.Id; + if (!record.HoldsAnyChoice) + return; + + try + { + await this.ModelComparisonStore.SaveResultAsync(record); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not store the results of a batch of the Model Comparison assistant."); + await MessageBus.INSTANCE.SendError(new(AppIcons.ASSISTANT_MODEL_COMPARISON, T("AI Studio could not save the results of this comparison for the statistics."))); + } + } + + /// + /// The culture of the active language, see ModelComparisonFormat.Culture. + /// + private static System.Globalization.CultureInfo DisplayCulture => ModelComparisonFormat.Culture; + + /// + /// Only the question is required. A comparison without a context is a comparison of how two + /// models answer a plain question, which is a fair thing to want to know. + /// + private string? ValidatingQuestion(string question) + { + if (string.IsNullOrWhiteSpace(question)) + return TA("Please provide a question or task for both models."); + + return null; + } + + /// + /// Checks a model of the comparison: either compared model or the judge. The selection only offers + /// providers which may be used, but it keeps what was chosen earlier, and a preset brings back what + /// was chosen when it was saved: a provider which the organization prohibited since, or one selected + /// before the confidence requirements were raised, must not be sent a request all the same. + /// + /// + /// ModelComparisonProviderAdapter refuses such a provider as well, but only once the run has + /// started, and then the model shows up as one which did not answer. Here the user is told before + /// that. + /// + private string? ValidatingComparedProvider(AIStudio.Settings.Provider provider) + { + return this.ValidatingProvider(provider) ?? ModelComparisonProviderCheck.IssueOf(this.SettingsManager, provider); + } + + /// + /// Checks the judge model. It may be the same provider as either compared model, the same as + /// the two compared models may be the same provider as each other: nothing here rules out a + /// strong model judging one it would also have been a fair pick for, or a model being compared + /// against itself to see how consistent it is with its own answers. + /// + /// + /// Silent until hasAttemptedCompare: this field mounts only once the judge is + /// switched on, often well after the form first rendered, and MudForm would otherwise flag it as + /// soon as it exists, before the user had any chance to fill it in. + /// + private string? ValidatingJudgeProvider(AIStudio.Settings.Provider provider) + { + return this.hasAttemptedCompare ? this.ValidatingComparedProvider(provider) : null; + } + + /// + /// Whether one of the fields of the switched-on judge has an issue: its model, or the expectations of the user + /// without the confirmation that the judge may decide by its own standards. + /// + private bool JudgeHasIssue => this.judgeEnabled && (this.ValidatingComparedProvider(this.judgeProvider) is not null || ModelComparisonJudgeExpectations.IssueOf(this.judgeInstructions, this.judgeOwnStandardsAccepted) is not null); +} diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor new file mode 100644 index 000000000..06b57b88d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor @@ -0,0 +1,92 @@ +@attribute [Route(Routes.ASSISTANT_MODEL_COMPARISON_TASKS)] +@inherits MSGComponentBase + + +
+ + + @ModelComparisonMode.TASKS.PageTitle + + + + + + + + + + + + + @if (this.openSeries is { } series) + { + + + + + @series.Name + + + + + + + + + @* A new series started from this one replaces it here, and it must not inherit the state of its view: *@ + + } + else + { + + @ModelComparisonSeriesTexts.WhatASeriesIs + + + + @T("New comparison series") + + + @TA("Your comparison series") + @if (this.allSeries is null) + { + + } + else if (this.allSeries.Count == 0) + { + @TA("You have no comparison series yet.") + } + else + { + + @TA("Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one.") + + + @foreach (var entry in this.allSeries) + { + + + + @entry.Name + @this.DescribeSeries(entry) + + + + @T("Open") + + + + + + + + + + + + + } + } + } + +
+
\ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor.cs new file mode 100644 index 000000000..c95c524dd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/AssistantModelComparisonTasks.razor.cs @@ -0,0 +1,211 @@ +using AIStudio.Chat.Embedded; +using AIStudio.Components; +using AIStudio.Dialogs; +using AIStudio.Dialogs.Settings; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The mode of the Model Comparison assistant in which the user works on real tasks with both models, one after the +/// other, and decides which chat helped more. +/// +/// +/// The page shows one of two views: the list of the comparison series or an opened series. A new series comes from +/// ModelComparisonSeriesDialog. A series lives in ModelComparisonStore, so the user can continue it after days or +/// weeks. +/// +public partial class AssistantModelComparisonTasks : MSGComponentBase +{ + [Inject] + private NavigationManager NavigationManager { get; init; } = null!; + + [Inject] + private ModelComparisonStore ModelComparisonStore { get; init; } = null!; + + [Inject] + private EmbeddedChatService ChatService { get; init; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + /// + /// All comparison series, or null while they load. + /// + private IReadOnlyList? allSeries; + + /// + /// The series the user opened, or null while the list shows. + /// + private ModelComparisonSeries? openSeries; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + if (!ModelComparisonAccess.CanOpen(this.SettingsManager, T("Model Comparison"))) + { + this.NavigationManager.NavigateTo(Routes.ASSISTANTS); + return; + } + + await this.ReloadSeriesAsync(); + } + + #endregion + + private async Task ReloadSeriesAsync() => this.allSeries = await this.ModelComparisonStore.ListSeriesAsync(); + + /// + /// Describes a series in the list without naming its models: their names would end the blindness. + /// + private string DescribeSeries(ModelComparisonSeries series) + { + var culture = Tools.PluginSystem.I18N.I.Culture; + var decided = series.Tasks.Count(task => task.State is ModelComparisonSeriesTaskState.DONE).CompactCount(); + var started = CommonTools.FormatTimestampToGeneral(series.CreatedAtUtc.ToLocalTime().DateTime, culture); + return series.IsResolved + ? string.Format(culture, T("Started {0}, decided tasks: {1}, result shown"), started, decided) + : string.Format(culture, T("Started {0}, decided tasks: {1}"), started, decided); + } + + /// + /// Lets the user set up a new series in its dialog, and opens the series once the dialog stored it. + /// + /// The series whose setup the new one takes over, or null for the preselected options. + private async Task CreateSeriesAsync(ModelComparisonSeries? basedOn) + { + var parameters = new DialogParameters + { + { dialog => dialog.BasedOn, basedOn } + }; + + var reference = await this.DialogService.ShowAsync(T("New comparison series"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled || result.Data is not ModelComparisonSeries series) + return; + + await this.ReloadSeriesAsync(); + this.OpenSeries(series); + } + + private void OpenSeries(ModelComparisonSeries series) => this.openSeries = series; + + private async Task CloseSeries() + { + this.openSeries = null; + await this.ReloadSeriesAsync(); + } + + /// + /// Asks for a new name of a series and stores it. The series is the one the list or the open view holds, so that + /// both show the new name right away. + /// + private async Task RenameSeriesAsync(ModelComparisonSeries series) + { + // The names of the other series, as stored now, so that the new name stays unique: + var takenNames = (await this.ModelComparisonStore.ListSeriesAsync()).Where(entry => entry.Id != series.Id).Select(entry => entry.Name).ToList(); + var parameters = new DialogParameters + { + { dialog => dialog.Message, TA("Enter a new name for this comparison series.") }, + { dialog => dialog.InputHeaderText, T("Name of the series") }, + { dialog => dialog.UserInput, series.Name }, + { dialog => dialog.ConfirmText, T("Rename") }, + { dialog => dialog.ConfirmColor, Color.Primary }, + + // The name rule rejects an empty name itself, in the same words as the dialog of a new series: + { dialog => dialog.AllowEmptyInput, true }, + { dialog => dialog.AdditionalValidation, candidate => ModelComparisonNames.SeriesNameIssue(candidate, takenNames) }, + }; + + var reference = await this.DialogService.ShowAsync(T("Rename series"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled || result.Data is not string name || ModelComparisonNames.SeriesNameIssue(name, takenNames) is not null) + return; + + var previousName = series.Name; + series.Name = name.Trim(); + try + { + await this.ModelComparisonStore.SaveSeriesAsync(series); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + series.Name = previousName; + this.Logger.LogError(exception, "Could not rename a comparison series of the Model Comparison assistant."); + await this.MessageBus.SendError(new(AppIcons.SAVE_FAILED, TA("AI Studio could not rename the comparison series. Please try again."))); + } + + if (this.openSeries is null) + await this.ReloadSeriesAsync(); + } + + /// + /// Deletes a series with its chats after asking. + /// + private async Task DeleteSeriesAsync(ModelComparisonSeries series) + { + var parameters = new DialogParameters + { + { dialog => dialog.Message, string.Format(TA("Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone."), series.Name) }, + }; + + var reference = await this.DialogService.ShowAsync(T("Delete series"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled) + return; + + // An answer which still runs would otherwise try to store its chat into the deleted series: + foreach (var task in series.Tasks) + { + await this.ChatService.ForgetAsync(task.ChatOne.ChatId); + await this.ChatService.ForgetAsync(task.ChatTwo.ChatId); + } + + try + { + await this.ModelComparisonStore.DeleteSeriesAsync(series.Id); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not delete a comparison series of the Model Comparison assistant."); + await this.MessageBus.SendError(new(AppIcons.DELETE, TA("AI Studio could not delete the comparison series. Please try again."))); + } + + await this.ReloadSeriesAsync(); + } + + /// + /// Shows what all stored comparisons say about the models, the series whose result the user saw included. + /// + /// + /// The user may delete all series there. Afterward, the list is read anew, and an open series which is gone + /// closes, which also stops its judge. + /// + private async Task OpenResultsDialog() + { + var reference = await this.DialogService.ShowAsync(T("Results of all comparisons"), DialogOptions.FULLSCREEN); + await reference.Result; + + await this.ReloadSeriesAsync(); + if (this.openSeries is { } series && this.allSeries!.All(entry => entry.Id != series.Id)) + this.openSeries = null; + } + + /// + /// Goes back to the choice between the two modes. + /// + private void SwitchMode() => this.NavigationManager.NavigateTo(Routes.ASSISTANT_MODEL_COMPARISON); + + /// + /// Opens the options of the assistant, which both modes share, as the mode Answer Variance does through its base class. + /// + private async Task OpenSettingsDialog() => await this.DialogService.ShowAsync(null, new DialogParameters(), DialogOptions.FULLSCREEN); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAccess.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAccess.cs new file mode 100644 index 000000000..182aad3a5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAccess.cs @@ -0,0 +1,24 @@ +using AIStudio.Settings; + +using ComponentKind = AIStudio.Tools.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Decides whether the pages of the Model Comparison assistant may be opened. +/// +/// +/// An organization can hide the assistant like any other. The tile asks the same question; the pages which do not +/// inherit AssistantBase ask here, because somebody might reach them without the tile, e.g., from a link or the +/// browser history. +/// +public static class ModelComparisonAccess +{ + /// + /// Checks whether the Model Comparison assistant may be opened with the given settings. + /// + /// The settings to check. + /// The name of the assistant for the log; without one, nothing gets logged. + /// True, when no configuration hides the assistant. + public static bool CanOpen(SettingsManager settingsManager, string assistantName = "") => settingsManager.IsAssistantVisible(ComponentKind.MODEL_COMPARISON_ASSISTANT, assistantName: assistantName); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswer.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswer.cs new file mode 100644 index 000000000..8ef4ccb1e --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswer.cs @@ -0,0 +1,37 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What one model answered, together with how the request went. +/// +public sealed record ModelComparisonAnswer +{ + /// + /// The name to show for the model. It says who wrote the answer, and is not shown until the + /// user has voted. + /// + public required string Label { get; init; } + + /// + /// The answer without the thinking of the model. Only an answer which is + /// Completed is complete: after a failure or a cancellation, this is whatever + /// arrived before, and it must not be shown as a result. + /// + public required string Text { get; init; } + + /// + /// Whether the model answered. False when the request failed, when the user canceled it, and + /// when the model ended without saying anything. + /// + public required bool Completed { get; init; } + + /// + /// The time from sending the request until the first piece of the answer arrived, or null when + /// nothing arrived. + /// + public TimeSpan? FirstTokenTime { get; init; } + + /// + /// The time from sending the request until the answer was complete, or null when it never was. + /// + public TimeSpan? TotalTime { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor new file mode 100644 index 000000000..ee0513bf1 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor @@ -0,0 +1,22 @@ +@inherits MSGComponentBase + + + + @if (this.VoteChoice is not null) + { + + } + + @this.Answer.Label + + @if (ModelComparisonOutcome.JudgeFavors(this.JudgeChoice, this.Model)) + { + + @T("Judge") + + } + + + @this.TimesText + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor.cs new file mode 100644 index 000000000..5c58df12b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCaption.razor.cs @@ -0,0 +1,56 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the summary of a batch says about the model of an answer: its name, how the user voted, whether the judge +/// preferred it, and how long it took. +/// +/// +/// Below the answer, not above it: an answer can be long, and what stands above it is out of sight by the time it has +/// been read. +/// +public partial class ModelComparisonAnswerCaption : MSGComponentBase +{ + /// + /// Gets or sets the answer, which knows who wrote it. + /// + [Parameter, EditorRequired] + public ModelComparisonAnswer Answer { get; set; } = null!; + + /// + /// Gets or sets the model whose answer this is. + /// + [Parameter, EditorRequired] + public ModelComparisonModelChoice Model { get; set; } + + /// + /// Gets or sets the model the user preferred in the run, or null without a vote. + /// + [Parameter] + public ModelComparisonModelChoice? VoteChoice { get; set; } + + /// + /// Gets or sets the model the judge preferred in the run, or null without a usable verdict. + /// + [Parameter] + public ModelComparisonModelChoice? JudgeChoice { get; set; } + + /// + /// Gets or sets further classes of the column, e.g., its order in the grid, which differs in a narrow window: + /// there, the caption follows right below its answer. + /// + [Parameter] + public string ItemClass { get; set; } = string.Empty; + + /// + /// Says how long the answer took: until the first piece arrived, and until it was complete. + /// + private string TimesText => string.Format( + ModelComparisonFormat.Culture, + T("First token after {0}, complete after {1}"), + ModelComparisonFormat.Duration(this.Answer.FirstTokenTime), + ModelComparisonFormat.Duration(this.Answer.TotalTime)); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor new file mode 100644 index 000000000..4872f5229 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor @@ -0,0 +1,18 @@ +@using AIStudio.Chat +@inherits MSGComponentBase + +
+ + @this.Title + + @if (this.Answer.Completed) + { + + } + else + { + + @T("This model did not answer.") + + } +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor.cs new file mode 100644 index 000000000..52e2508c5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAnswerCard.razor.cs @@ -0,0 +1,72 @@ +using AIStudio.Chat; +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The card of one answer of the Model Comparison assistant, with its title above. +/// +/// +/// The same markup for both answers of a run, so that nothing but the position tells them apart. The card fills the +/// column of the grid it stands in; from the breakpoint md of MudBlazor on, both columns are in one row, so the cards +/// are equally high however long the answers are. That is why the names of the models are not part of the card: how +/// long a name is would make the cards differ, see ModelComparisonAnswerCaption. In a narrower window, the second card +/// stands below the first one. The column itself belongs to the page, see ModelComparisonVoteColumns. +/// +public partial class ModelComparisonAnswerCard : MSGComponentBase +{ + /// + /// Gets or sets the title: "Answer A" or "Answer B" while the user votes, "First model" or "Second model" in the + /// summary. + /// + [Parameter, EditorRequired] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the answer. + /// + [Parameter, EditorRequired] + public ModelComparisonAnswer Answer { get; set; } = null!; + + /// + /// Gets or sets when the run ended. Both answers of a run show this one time: a time of their own would tell the + /// faster model from the slower one. + /// + [Parameter] + public DateTimeOffset Time { get; set; } + + /// + /// Gets or sets the classes of the card, see ModelComparisonOutcome.CardClassOf. + /// + [Parameter] + public string CardClass { get; set; } = ModelComparisonOutcome.FILL_THE_COLUMN; + + /// + /// The answer as the chat component wants it. Built once per answer, and not on every render: the chat component + /// compares what it is given to decide whether anything has to be drawn again, and a new one would make it parse the + /// whole Markdown again. + /// + private ContentText content = new(); + + /// + /// The answer which content was built from. + /// + private ModelComparisonAnswer? shownAnswer; + + #region Overrides of ComponentBase + + protected override void OnParametersSet() + { + if (!ReferenceEquals(this.shownAnswer, this.Answer)) + { + this.shownAnswer = this.Answer; + this.content = new ContentText { Text = this.Answer.Text }; + } + + base.OnParametersSet(); + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor new file mode 100644 index 000000000..c288fec11 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor @@ -0,0 +1,6 @@ +@inherits MSGComponentBase + + + + @this.Text + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor.cs new file mode 100644 index 000000000..59a55b2d6 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonAssessment.razor.cs @@ -0,0 +1,45 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How sure the results are that a model is better or worse, with an icon and a color, so that the assessment never +/// depends on the color alone. +/// +public partial class ModelComparisonAssessment : MSGComponentBase +{ + /// + /// Gets or sets what the assessment says. + /// + [Parameter, EditorRequired] + public string Text { get; set; } = string.Empty; + + /// + /// Gets or sets how sure the results are. + /// + [Parameter, EditorRequired] + public ModelComparisonSignificance Significance { get; set; } + + /// + /// Gets or sets whether the assessment is about a pair: a pair which one model won clearly gets the icon and the + /// color of the better one, since one of the two is always clearly worse then. + /// + [Parameter] + public bool IsPair { get; set; } + + private string Icon => this.Significance switch + { + ModelComparisonSignificance.CLEARLY_BETTER => AppIcons.CLEARLY_BETTER, + ModelComparisonSignificance.CLEARLY_WORSE => this.IsPair ? AppIcons.CLEARLY_BETTER : AppIcons.CLEARLY_WORSE, + _ => AppIcons.NOT_YET_CLEAR, + }; + + private Color Color => this.Significance switch + { + ModelComparisonSignificance.CLEARLY_BETTER => Color.Success, + ModelComparisonSignificance.CLEARLY_WORSE => this.IsPair ? Color.Success : Color.Error, + _ => Color.Default, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatch.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatch.cs new file mode 100644 index 000000000..b58c475b9 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatch.cs @@ -0,0 +1,224 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The runs of one batch as the user goes through them: which arrived, which the user voted on, and which run the user +/// looks at right now. +/// +/// +/// The runs are kept in the order they arrived, not the order they were started in: every run asks the same two models +/// the same question, and nothing tells "run 3" from "run 7" beyond how long each happened to take. They arrive one at +/// a time, so the user can vote on the first one without waiting for the slowest. +/// +/// Not thread-safe. The assistant changes and reads it on the dispatcher of its page only. +/// +/// How many runs the batch makes. +public sealed class ModelComparisonBatch(int runCount) +{ + private readonly List entries = []; + + /// + /// The vote of the user for each run, at the same index as its entry. Null while the vote is still to come, or when + /// there is none. + /// + private readonly List votes = []; + + /// + /// The ID of the batch, which names the file of its results, see ModelComparisonRecord. + /// + public Guid Id { get; private init; } = Guid.NewGuid(); + + /// + /// How many runs the batch makes. + /// + public int RunCount { get; } = runCount; + + /// + /// The runs which arrived so far, in the order they arrived. + /// + public IReadOnlyList Entries => this.entries; + + /// + /// Which run the user looks at right now, by its position. Moves on only once the vote is in, once the user chose to + /// skip the run, or once the user saw that the run has nothing to vote on, so it never passes a run the user has not + /// seen; only SkipRemainingVotes and Settle jump to the end. + /// + public int CurrentIndex { get; private set; } + + /// + /// Whether the user chose to stop voting on the runs of this batch. Every run still arrives, and the judge, if there + /// is one, still judges it, but none is shown on its own anymore: the summary appears once the batch has settled. + /// + public bool SkipsRemainingVotes { get; private set; } + + /// + /// The run the user looks at right now, or null before it arrived and once the user is through. + /// + public ModelComparisonBatchEntry? CurrentEntry => this.CurrentIndex < this.entries.Count ? this.entries[this.CurrentIndex] : null; + + /// + /// Whether the user is through with every run of the batch. + /// + public bool IsFinished => this.CurrentIndex >= this.RunCount; + + /// + /// Whether CurrentEntry is the last run of the batch, so that the button which moves past a run without answers + /// says "Finish" instead of "Next run". Both do the same: after the last run, the summary follows. + /// + public bool IsLastEntry => this.CurrentIndex + 1 >= this.RunCount; + + /// + /// Adds a run which arrived. + /// + /// The run. + public void Add(ModelComparisonBatchEntry entry) + { + this.entries.Add(entry); + this.votes.Add(null); + } + + /// + /// Takes the vote of the user for CurrentEntry and moves on to the next run. + /// + /// + /// The vote is final, and nothing about the run is revealed before the batch is over: neither the names of the + /// models nor the verdict of the judge. A user who knew which model wrote which answer in one run would recognize + /// its style in the next one, and the votes after that would no longer be blind. + /// + /// The column the user preferred, or a tie. + /// Whether the vote was taken; not for a run without both answers, which has nothing to vote on. + public bool Vote(ModelComparisonVote vote) + { + if (this.CurrentEntry is not { Result.BothCompleted: true }) + return false; + + this.votes[this.CurrentIndex] = vote; + this.CurrentIndex++; + return true; + } + + /// + /// Moves on past a run which has nothing to vote on, because at least one model did not answer. + /// + /// Whether the batch moved on; not for a run with both answers, which takes a vote or a skip. + public bool GoToNextEntry() + { + if (this.CurrentEntry is not { Result.BothCompleted: false }) + return false; + + this.CurrentIndex++; + return true; + } + + /// + /// Moves on to the next run without a vote on the current one, although it has something to vote on: the choice + /// of the user, unlike GoToNextEntry, which moves past a run with nothing to vote on. + /// + public void SkipThisRun() => this.CurrentIndex++; + + /// + /// Stops asking for votes on the runs which are left. + /// + /// Whether runs are still on their way. Then Settle jumps to the summary once the last one arrived; otherwise, nothing is left to wait for. + public void SkipRemainingVotes(bool runsStillArriving) + { + this.SkipsRemainingVotes = true; + if (!runsStillArriving) + this.CurrentIndex = this.RunCount; + } + + /// + /// Ends the batch once no run is left on its way. + /// + /// + /// A stopped batch goes straight to the summary, the same as one whose remaining votes the user skipped: there is + /// no point in asking for votes on runs the user chose to stop waiting for. + /// + /// Whether the user stopped the batch. + public void Settle(bool canceled) + { + if (this.SkipsRemainingVotes || canceled) + this.CurrentIndex = this.RunCount; + } + + /// + /// Where a run stands, for its badge in the progress of the batch. + /// + /// + /// Nothing tracks which run is which once all are started: they are interchangeable, and only how many arrived is + /// known. So the status of a run which did not arrive yet counts positions, not identities: the runs nearest to + /// arriving are shown as the ones asking the models right now, which is true in substance even when it is not true + /// of that exact run. + /// + /// The position of the run, zero-based. + /// The status. + public ModelComparisonRunStatus StatusOf(int index) + { + if (index >= this.entries.Count) + return index < this.entries.Count + ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS + ? ModelComparisonRunStatus.RUNNING + : ModelComparisonRunStatus.QUEUED; + + if (!this.entries[index].Result.BothCompleted) + return ModelComparisonRunStatus.NO_USABLE_ANSWER; + + if (this.votes[index] is not null) + return ModelComparisonRunStatus.VOTED; + + return index < this.CurrentIndex + ? ModelComparisonRunStatus.VOTE_SKIPPED + : ModelComparisonRunStatus.AWAITING_VOTE; + } + + /// + /// Whether the run at this position arrived with a usable verdict of the judge. + /// + /// The position of the run, zero-based. + public bool HasJudgeVerdict(int index) => index < this.entries.Count && this.entries[index].Result.Judge is { Completed: true }; + + /// + /// Which model the user preferred in the run at this position. + /// + /// The position of an arrived run, zero-based. + /// The model, a tie, or null when the user did not vote on the run. + public ModelComparisonModelChoice? VoteChoiceAt(int index) => this.votes[index] is { } vote + ? this.entries[index].PresentationOrder.ToModelChoice(vote) + : null; + + /// + /// Which model the judge preferred in the run at this position. + /// + /// The position of an arrived run, zero-based. + /// The model, a tie, or null when the run has no usable verdict of a judge. + public ModelComparisonModelChoice? JudgeChoiceAt(int index) => this.entries[index].Result.Judge is { Completed: true, Preferred: { } preferred } + ? this.entries[index].PresentationOrder.ToModelChoice(preferred) + : null; + + /// + /// Counts the votes of the user across the runs which arrived. + /// + public ModelComparisonTally TallyVotes() => ModelComparisonTally.Of(Enumerable.Range(0, this.entries.Count).Select(this.VoteChoiceAt)); + + /// + /// Counts the verdicts of the judge across the runs which arrived. + /// + public ModelComparisonTally TallyJudgeVerdicts() => ModelComparisonTally.Of(Enumerable.Range(0, this.entries.Count).Select(this.JudgeChoiceAt)); + + /// + /// Copies the batch, for a snapshot of the assistant session: a snapshot must not change with the batch it was + /// taken of. The entries themselves are shared, since nothing changes them. + /// + /// The copy. + public ModelComparisonBatch Copy() + { + var copy = new ModelComparisonBatch(this.RunCount) + { + Id = this.Id, + CurrentIndex = this.CurrentIndex, + SkipsRemainingVotes = this.SkipsRemainingVotes, + }; + + copy.entries.AddRange(this.entries); + copy.votes.AddRange(this.votes); + return copy; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchEntry.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchEntry.cs new file mode 100644 index 000000000..5e3206a61 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchEntry.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// One run within a batch: its own random presentation order, and what came of it. +/// +/// +/// The presentation order travels with its result because it takes both to translate a +/// ModelComparisonVote -- the user's own vote, or the judge's -- back into a model: the +/// same column means a different model in every run. +/// +public sealed record ModelComparisonBatchEntry +{ + public required ModelComparisonPresentationOrder PresentationOrder { get; init; } + + public required ModelComparisonRunResult Result { get; init; } +} diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchRunner.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchRunner.cs new file mode 100644 index 000000000..50f0d46b5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchRunner.cs @@ -0,0 +1,127 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Starts the same comparison several times in a row, to see how consistently the two models -- and +/// an optional judge -- come out the same way. +/// +/// +/// Each run is independent: its own pair of answers, its own random presentation order, and its own +/// judge verdict, because that variation between runs is exactly what a batch is meant to surface. A +/// single, shared draw would defeat the point. +/// +/// This starts every run at once and hands back one task per run, rather than awaiting them itself: +/// the caller decides whether to wait for all of them together (Task.WhenAll) or to react to +/// each one as it finishes (Task.WhenEach), for example to reveal runs to the user as they +/// become ready instead of only once the slowest of them has answered. "Started" only means the task +/// exists and is queued, though: MAX_CONCURRENT_RUNS of them are actually asking a +/// model at any one time, never more, because a whole batch's worth of requests landing on a model at +/// once is exactly what queues up and slows every one of them down on a provider with limited +/// concurrent capacity -- a self-hosted one most of all. A model comparison is not something run many +/// times a day, so trading wall-clock time for going easier on the model is the right default. +/// +/// Start is called from a Blazor Server page, on its own synchronisation context, so +/// RunOneAsync uses ConfigureAwait(false) throughout, the same as +/// ModelComparisonRunner does further down: none of this is UI work, and resuming on +/// that context regardless would only queue up behind whatever the page itself is waiting to do, +/// a click included. +/// +/// Runs one comparison. Reused for every run of a batch, since the two compared +/// models and the judge are stateless requests, not a resource a run could use up. +public sealed class ModelComparisonBatchRunner(ModelComparisonRunner runner) +{ + /// + /// How many runs may be actually asking a model at once. Fixed, not a setting: this exists to + /// protect whatever the batch is running against, not to be tuned per comparison. Public so the + /// UI can show which of the runs still outstanding are actually in flight right now, rather than + /// still queued behind this same limit. + /// + public const int MAX_CONCURRENT_RUNS = 3; + + private readonly SemaphoreSlim concurrencyLimit = new(MAX_CONCURRENT_RUNS, MAX_CONCURRENT_RUNS); + + /// The first model, asked fresh in every run. + /// The second model, asked fresh in every run. + /// What both models are asked, the same in every run. + /// How many independent runs to start. At least one. + /// Cancels every run of the batch, in flight or still queued. + /// The optional judge, asked in every run once its two answers are in. + /// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations. Ignored without a . + /// One task per run, already started, in no particular order of completion. + public IReadOnlyList> Start( + ModelComparisonParticipant first, + ModelComparisonParticipant second, + ModelComparisonRequest request, + int runCount, + CancellationToken token, + ModelComparisonParticipant? judge = null, + string judgeInstructions = "") + { + ArgumentOutOfRangeException.ThrowIfLessThan(runCount, 1); + + var tasks = new Task[runCount]; + for (var i = 0; i < runCount; i++) + tasks[i] = this.RunOneAsync(first, second, request, token, judge, judgeInstructions); + + return tasks; + } + + /// + /// Waits for a free slot among MAX_CONCURRENT_RUNS, then runs one comparison. + /// + /// + /// Never throws, a queued run included: cancellation while still waiting for a slot is reported + /// the same way ModelComparisonRunner reports a model which did not answer, not as + /// a fault on the task -- every run in a batch is meant to end up as an entry to show, whatever + /// happened to it. + /// + private async Task RunOneAsync( + ModelComparisonParticipant first, + ModelComparisonParticipant second, + ModelComparisonRequest request, + CancellationToken token, + ModelComparisonParticipant? judge, + string judgeInstructions) + { + var presentationOrder = Random.Shared.Next(2) is 0 + ? ModelComparisonPresentationOrder.FIRST_MODEL_FIRST + : ModelComparisonPresentationOrder.SECOND_MODEL_FIRST; + + try + { + await this.concurrencyLimit.WaitAsync(token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + return new ModelComparisonBatchEntry + { + PresentationOrder = presentationOrder, + Result = new ModelComparisonRunResult + { + First = NotAnswered(first.Label), + Second = NotAnswered(second.Label), + }, + }; + } + + try + { + var result = await runner.RunAsync(first, second, request, token, presentationOrder, judge, judgeInstructions).ConfigureAwait(false); + return new ModelComparisonBatchEntry + { + PresentationOrder = presentationOrder, + Result = result, + }; + } + finally + { + this.concurrencyLimit.Release(); + } + } + + private static ModelComparisonAnswer NotAnswered(string label) => new() + { + Label = label, + Text = string.Empty, + Completed = false, + }; +} diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor new file mode 100644 index 000000000..e3b629396 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor @@ -0,0 +1,75 @@ +@inherits MSGComponentBase + + + + @this.Heading + + + @if (this.Batch.Entries.Count > 0) + { + + @ModelComparisonResultTexts.WhatTheBatchSummaryShows + + + + + + + @this.Batch.Entries[0].Result.First.Label + @T("Equally good") + @this.Batch.Entries[0].Result.Second.Label + + + + + @TA("Your votes") + @this.voteTally.FirstModelWins + @this.voteTally.Ties + @this.voteTally.SecondModelWins + + @if (this.judgeTally is { } shownJudgeTally) + { + + @T("Judge verdicts") + @shownJudgeTally.FirstModelWins + @shownJudgeTally.Ties + @shownJudgeTally.SecondModelWins + + } + + + + @if (this.voteTally.UnscoredRunCount > 0) + { + + @string.Format(ModelComparisonFormat.Culture, TA("{0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above."), this.voteTally.UnscoredRunCount, this.voteTally.TotalRunCount) + + } + @if (this.judgeTally is { UnscoredRunCount: > 0 } incompleteJudgeTally) + { + + @string.Format(ModelComparisonFormat.Culture, T("{0} of {1} runs have no usable judge verdict. They are not counted above."), incompleteJudgeTally.UnscoredRunCount, incompleteJudgeTally.TotalRunCount) + + } + } + + +@if (this.Batch.Entries.Count > 0) +{ + @* One run at a time: two open runs, each with two long answers, are hard to tell apart. *@ + + @foreach (var index in Enumerable.Range(0, this.Batch.Entries.Count)) + { + + } + +} + + + + @T("Start a new request") + + + @T("Results of all comparisons") + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor.cs new file mode 100644 index 000000000..5d6fbc9ce --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchSummary.razor.cs @@ -0,0 +1,61 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The summary of a finished batch, with the names of the models: the votes of the user and the verdicts of the judge +/// in total, and every run on its own. +/// +public partial class ModelComparisonBatchSummary : MSGComponentBase +{ + /// + /// Gets or sets the finished batch. + /// + [Parameter, EditorRequired] + public ModelComparisonBatch Batch { get; set; } = null!; + + /// + /// Gets or sets whether a judge took part in the batch. + /// + [Parameter] + public bool JudgeEnabled { get; set; } + + /// + /// Gets or sets what happens when the user wants to start over with the request. + /// + [Parameter] + public EventCallback StartNewBatch { get; set; } + + /// + /// Gets or sets what happens when the user wants to see the results of all comparisons. + /// + [Parameter] + public EventCallback OpenResults { get; set; } + + /// + /// The votes of the user in total, counted once per render. + /// + private ModelComparisonTally voteTally = ModelComparisonTally.NONE; + + /// + /// The verdicts of the judge in total, or null without a judge. + /// + private ModelComparisonTally? judgeTally; + + #region Overrides of ComponentBase + + protected override void OnParametersSet() + { + this.voteTally = this.Batch.TallyVotes(); + this.judgeTally = this.JudgeEnabled ? this.Batch.TallyJudgeVerdicts() : null; + base.OnParametersSet(); + } + + #endregion + + private string Heading => this.Batch.RunCount > 1 + ? string.Format(ModelComparisonFormat.Culture, T("All {0} runs are done."), this.Batch.RunCount) + : T("Done."); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor new file mode 100644 index 000000000..0ce6a7601 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor @@ -0,0 +1,49 @@ +@inherits MSGComponentBase + +@if (this.Batch is { SkipsRemainingVotes: true, IsFinished: false }) +{ + + + + @TA("You skipped the remaining votes. The summary follows once the remaining runs are done.") + + +} +else if (this.IsComparing && this.Batch is { CurrentEntry: null } waitingBatch) +{ + + @* The same waiting as in the chat: a bar, and lines where the answers will appear. Both columns look alike, so this says nothing about the models. *@ + + + + @(waitingBatch.Entries.Count == 0 ? T("Both models are answering. This can take a while.") : T("Waiting for the next run...")) + + + + + + @T("Answer A") + + + + + + + + @T("Answer B") + + + + + + + +} +else if (this.Batch is { CurrentEntry: { } entry } currentBatch) +{ + +} +else if (this.Batch is { IsFinished: true } finishedBatch) +{ + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor.cs new file mode 100644 index 000000000..b4f915a2a --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonBatchView.razor.cs @@ -0,0 +1,71 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Everything which follows the Compare button of the Model Comparison assistant: the wait, the answers, the vote, and +/// the summary. +/// +/// +/// The assistant renders it below its submit row, see AssistantBase.BelowSubmitContent: this is where the button has to +/// stay above the vote, and content of the body would come before it. The batch changes only on the dispatcher of the +/// page, which renders this view anew after every change. +/// +public partial class ModelComparisonBatchView : MSGComponentBase +{ + /// + /// Gets or sets the batch which runs or ran, or null while the form is shown. + /// + [Parameter] + public ModelComparisonBatch? Batch { get; set; } + + /// + /// Gets or sets whether at least one run of the batch is still in flight. + /// + [Parameter] + public bool IsComparing { get; set; } + + /// + /// Gets or sets whether a judge takes part in the batch. + /// + [Parameter] + public bool JudgeEnabled { get; set; } + + /// + /// Gets or sets what happens when the user votes on the current run. + /// + [Parameter] + public EventCallback Vote { get; set; } + + /// + /// Gets or sets what happens when the user skips the vote on the current run. + /// + [Parameter] + public EventCallback SkipThisRun { get; set; } + + /// + /// Gets or sets what happens when the user skips the votes on all remaining runs. + /// + [Parameter] + public EventCallback SkipRemainingVotes { get; set; } + + /// + /// Gets or sets what happens when the user moves past a run which has nothing to vote on. + /// + [Parameter] + public EventCallback NextEntry { get; set; } + + /// + /// Gets or sets what happens when the user wants to start over with the request. + /// + [Parameter] + public EventCallback StartNewBatch { get; set; } + + /// + /// Gets or sets what happens when the user wants to see the results of all comparisons. + /// + [Parameter] + public EventCallback OpenResults { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonCharts.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonCharts.cs new file mode 100644 index 000000000..3b5d0721a --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonCharts.cs @@ -0,0 +1,74 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the charts of the results of the Model Comparison assistant have in common. +/// +/// +/// The colors are fixed rather than taken from the theme, because MudBlazor paints the bars with the colors of the +/// palette as they are. Blue for wins, gray for ties, and red for losses read on the light and on the dark theme alike, +/// and stay apart for readers with a color vision deficiency; the axes and the legend follow the theme on their own. On +/// the dark theme, blue and red stay slightly below a contrast of 3:1, so every chart comes with a table of the same +/// numbers, which reads without any color. +/// +public static class ModelComparisonCharts +{ + /// + /// The colors of wins, ties, and losses, in the order of the series. + /// + public static readonly string[] OUTCOME_PALETTE = ["#2a78d6", "#898781", "#e34948"]; + + /// + /// The colors of the time until the first token and until the complete answer. + /// + public static readonly string[] SPEED_PALETTE = ["#2a78d6", "#d95926"]; + + /// + /// The colors of the first model of a pair, of a tie, and of the second model. Not the red of a loss: a win of + /// the second model is a loss of the first one, and red would favor one side. + /// + public static readonly string[] PAIR_PALETTE = ["#2a78d6", "#898781", "#d95926"]; + + /// + /// The color of a chart with a single series, e.g., the effort with each model. + /// + public static readonly string[] SINGLE_PALETTE = ["#2a78d6"]; + + /// + /// Above this many models, the names below the bars are tilted, so that long names do not run into each other. + /// + private const int MAX_UPRIGHT_LABELS = 4; + + /// + /// About how many steps the vertical axis of a chart shows. + /// + private const int AXIS_STEP_COUNT = 5; + + /// + /// Gets the names of the models below the bars of a chart. + /// + /// The models, in the order of the bars. + /// The names. + public static string[] ModelLabels(IEnumerable models) => [.. models.Select(model => model.Label)]; + + /// + /// Gets by how many degrees the names below the bars are tilted. + /// + /// How many models the chart shows. + /// The angle, 0 for upright names. + public static int LabelRotation(int modelCount) => modelCount > MAX_UPRIGHT_LABELS ? 30 : 0; + + /// + /// Gets a step for the vertical axis: a whole 1, 2, or 5 times a power of ten, so that the axis shows about + /// AXIS_STEP_COUNT steps up to the largest value. Counts have no fractions, and seconds need none on an axis. + /// + /// The values of the chart. + /// The step. + public static int AxisStep(IEnumerable values) + { + var maximum = values.DefaultIfEmpty(0).Max(); + for (var magnitude = 1; ; magnitude *= 10) + foreach (var factor in (int[])[1, 2, 5]) + if (maximum / (factor * magnitude) <= AXIS_STEP_COUNT) + return factor * magnitude; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonChatJudgeRequest.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonChatJudgeRequest.cs new file mode 100644 index 000000000..8a1e1c903 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonChatJudgeRequest.cs @@ -0,0 +1,42 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the judge of a comparison series is asked: the two chats of a task, as transcripts, see ChatTranscript. +/// +/// +/// The judge is as blind as the user: the chats go in as "Chat 1" and "Chat 2", the names the user sees, never by +/// model. It answers in the same JSON as the judge of the stability mode, see ModelComparisonJudgeFormat.CHATS. +/// +/// The first chat. +/// The second chat. +/// The language of the reasoning, by its English name, or an empty text to leave it to the judge. +/// +/// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations; +/// or an empty text, when the user confirmed that the judge may decide by its own standards. +/// +public sealed record ModelComparisonChatJudgeRequest(string TranscriptOfChatOne, string TranscriptOfChatTwo, string LanguageName, string Expectations) +{ + /// + /// Builds the prompt for the judge model, asking for one JSON object and nothing else. + /// + /// An identifier against caching gateways, see ModelComparisonJudgeFormat.WithCacheBuster. + /// The user prompt the judge model receives. + public string ToPrompt(string cacheBuster = "") + { + var prompt = $$""" + You judge two chats in which a user worked on the same task, each time with another AI model. Both chats began with the same first message; afterward, the user may have written different messages in each. You do not know, and must not guess, which model answered in which chat, so judge only by what the chats say. + + Judge in which chat the AI model helped the user more with the task: how correct, complete, and useful its answers were, and how little the user had to correct it or ask again. A call of a tool is a line of its own, without its result. Attached files are named, without their content.{{ModelComparisonJudgeFormat.ExpectationsOfTheUser(this.Expectations)}} + + # Chat 1 + {{this.TranscriptOfChatOne}} + + # Chat 2 + {{this.TranscriptOfChatTwo}} + + Decide in which chat the AI model helped more, or whether both helped equally. {{ModelComparisonJudgeFormat.CHATS.AnswerInstructions(this.LanguageName)}} + """; + + return ModelComparisonJudgeFormat.WithCacheBuster(prompt, cacheBuster); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor new file mode 100644 index 000000000..96315f286 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor @@ -0,0 +1,23 @@ +@inherits MSGComponentBase + +@* Two charts rather than one with two axes: messages and minutes do not share a scale. *@ + + @if (this.messageEfforts.Count > 0) + { + + + @TA("Your messages on average") + + + + } + @if (this.timeEfforts.Count > 0) + { + + + @T("Median time") + + + + } + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor.cs new file mode 100644 index 000000000..dfcd7f5ea --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonEffortCharts.razor.cs @@ -0,0 +1,70 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The effort with each model of a comparison series as two bar charts: how many messages the user wrote on average, +/// and the median time the user worked with it. A model without a value is left out of that chart. +/// +public partial class ModelComparisonEffortCharts : MSGComponentBase +{ + /// + /// Gets or sets the effort with each model, in the order of the models of the series. + /// + [Parameter, EditorRequired] + public IReadOnlyList Efforts { get; set; } = []; + + /// + /// The efforts with an average of the messages. + /// + private List messageEfforts = []; + + /// + /// The efforts with a median time. + /// + private List timeEfforts = []; + + #region Overrides of ComponentBase + + protected override void OnParametersSet() + { + this.messageEfforts = [.. this.Efforts.Where(effort => effort.AverageUserMessages is not null)]; + this.timeEfforts = [.. this.Efforts.Where(effort => effort.MedianDuration is not null)]; + base.OnParametersSet(); + } + + #endregion + + private List> MessageSeries => + [ + new() { Name = TA("Your messages on average"), Data = new([.. this.messageEfforts.Select(effort => effort.AverageUserMessages!.Value)]) }, + ]; + + private List> TimeSeries => + [ + new() { Name = T("Median time"), Data = new([.. this.timeEfforts.Select(effort => effort.MedianDuration!.Value.TotalMinutes)]) }, + ]; + + /// + /// A single series needs no legend: the title above the chart names it. + /// + private BarChartOptions MessageChartOptions => new() + { + ChartPalette = ModelComparisonCharts.SINGLE_PALETTE, + ShowLegend = false, + YAxisTicks = ModelComparisonCharts.AxisStep(this.messageEfforts.Select(effort => effort.AverageUserMessages!.Value)), + YAxisToStringFunc = value => ((long)value).CompactCount(), + XAxisLabelRotation = ModelComparisonCharts.LabelRotation(this.messageEfforts.Count), + }; + + private BarChartOptions TimeChartOptions => new() + { + ChartPalette = ModelComparisonCharts.SINGLE_PALETTE, + ShowLegend = false, + YAxisTicks = ModelComparisonCharts.AxisStep(this.timeEfforts.Select(effort => effort.MedianDuration!.Value.TotalMinutes)), + YAxisToStringFunc = value => ModelComparisonFormat.Minutes(TimeSpan.FromMinutes(value)), + XAxisLabelRotation = ModelComparisonCharts.LabelRotation(this.timeEfforts.Count), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonFormat.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonFormat.cs new file mode 100644 index 000000000..3f024ff41 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonFormat.cs @@ -0,0 +1,68 @@ +using System.Globalization; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Formats the numbers which the Model Comparison assistant shows, in the culture of the active language. +/// +public static class ModelComparisonFormat +{ + /// + /// Gets the culture of the active language. Qualified with its namespace because the namespace of the localization + /// assistant, AIStudio.Assistants.I18N, hides the class I18N from every assistant. + /// + public static CultureInfo Culture => Tools.PluginSystem.I18N.I.Culture; + + /// + /// Gets the English name of the active language, without a region ("German", not "German (Germany)"), for an + /// instruction which the models and the judges understand regardless of their own default language. The culture + /// carries a region, so its neutral parent names the language alone, except for the invariant culture, which has + /// no parent to ask. + /// + public static string LanguageName => Culture.IsNeutralCulture || Culture.Equals(CultureInfo.InvariantCulture) + ? Culture.EnglishName + : Culture.Parent.EnglishName; + + /// + /// Formats seconds with one decimal place, e.g., "2.5 s". + /// + /// The seconds. + /// The seconds with their unit. + public static string Seconds(double seconds) => $"{seconds.ToString("0.0", Culture)} s"; + + /// + /// Formats a duration in seconds. + /// + /// The duration, or null when it is not known. + /// The seconds with their unit, or a dash. + public static string Duration(TimeSpan? duration) => duration is { } value + ? Seconds(value.TotalSeconds) + : "-"; + + /// + /// Formats a duration in minutes with one decimal place, e.g., "12.5 min". + /// + /// The duration, or null when it is not known. + /// The minutes with their unit, or a dash. + public static string Minutes(TimeSpan? duration) => duration is { } value + ? $"{value.TotalMinutes.ToString("0.0", Culture)} min" + : "-"; + + /// + /// Formats a number with one decimal place, e.g., an average. + /// + /// The number, or null when it is not known. + /// The number, or a dash. + public static string Decimal(double? number) => number is { } value + ? value.ToString("0.0", Culture) + : "-"; + + /// + /// Formats a share as a whole percentage. + /// + /// The share between 0 and 1, or null when it is not known. + /// The percentage, or a dash. + public static string Share(double? share) => share is { } value + ? value.ToString("P0", Culture) + : "-"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJson.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJson.cs new file mode 100644 index 000000000..553c09aa4 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJson.cs @@ -0,0 +1,31 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The JSON options of the files of the Model Comparison assistant. +/// +internal static class ModelComparisonJson +{ + /// + /// The options with which the assistant writes and reads its files. + /// + /// + /// The files stay readable for people who look into the data folder: indented, enums by name, and umlauts as they + /// are rather than as escapes. The relaxed encoder is fine here, because nothing embeds these files into HTML. A + /// field this version does not know makes a file unreadable rather than letting a save drop it; the store then + /// leaves the file alone. + /// + internal static readonly JsonSerializerOptions PERSISTENCE = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = false, + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + Converters = { new JsonStringEnumConverter() }, + TypeInfoResolver = ModelComparisonJsonContext.Default, + }.AsReadOnly(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJsonContext.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJsonContext.cs new file mode 100644 index 000000000..6728a09b5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJsonContext.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The contracts with which System.Text.Json reads and writes the JSON of the Model Comparison assistant, built at +/// compile time. +/// +[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] +[JsonSerializable(typeof(ModelComparisonJudgeAnswer))] +[JsonSerializable(typeof(ModelComparisonPreset))] +[JsonSerializable(typeof(ModelComparisonRecord))] +[JsonSerializable(typeof(ModelComparisonSeries))] +internal sealed partial class ModelComparisonJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAgreement.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAgreement.cs new file mode 100644 index 000000000..09a08233d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAgreement.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How often a judge chose the same as the user, across the runs which hold both a vote and a verdict of this judge. +/// +public sealed record ModelComparisonJudgeAgreement +{ + /// + /// The judge, with the name it had in its latest comparison. + /// + public required ModelComparisonModelIdentity Judge { get; init; } + + /// + /// How many runs the judge chose the same as the user: the same model, or a tie both times. + /// + public int Agreements { get; init; } + + /// + /// How many runs hold both a vote of the user and a verdict of this judge. + /// + public int ComparedRuns { get; init; } + + /// + /// The share of the compared runs in which the judge chose the same as the user, or null without such runs. + /// + public double? Share => this.ComparedRuns == 0 ? null : (double)this.Agreements / this.ComparedRuns; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAnswer.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAnswer.cs new file mode 100644 index 000000000..b069d7d86 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeAnswer.cs @@ -0,0 +1,21 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The JSON object the judge is asked to answer with, as it arrives, before it is checked. +/// +/// +/// Both values may be missing or null in what a model writes: ModelComparisonJudgeVerdict.Parse decides +/// what a usable verdict is, not the reading of the JSON. +/// +public sealed record ModelComparisonJudgeAnswer +{ + /// + /// The column the judge preferred: "A", "B", or "TIE". + /// + public string? Preferred { get; set; } + + /// + /// Why the judge decided as it did. + /// + public string? Reasoning { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor new file mode 100644 index 000000000..aff5b93d4 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor @@ -0,0 +1,16 @@ +@inherits MSGComponentBase + +@TA("Your expectations, values, and goals") + + @TA("What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models.") + + +@if (!this.HasExpectations) +{ + + + @TA("Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do.") + + + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor.cs new file mode 100644 index 000000000..85c6483bd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeExpectations.razor.cs @@ -0,0 +1,109 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Asks the user for the expectations, values, and goals the judge applies, and, while there are none, for the +/// confirmation that the judge may decide by its own standards. +/// +/// +/// Both modes share this part of the judge. Without the standards of the user, the judge decides by what it learned in +/// training, with the bias of those who trained it. Like the warning about a cloud embedding, see +/// DataSourceCloudEmbeddingWarning, the user has to confirm that explicitly. The switch is a field of the form around +/// this component, which checks it when the user starts; IssueOf checks the same for code outside the form. +/// +public partial class ModelComparisonJudgeExpectations : MSGComponentBase +{ + private new static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(ModelComparisonJudgeExpectations).Namespace, nameof(ModelComparisonJudgeExpectations)); + + /// + /// How long the field waits after the last keystroke before it passes the expectations on. The switch appears or + /// disappears with them, so it follows the typing without flickering. + /// + private const double DEBOUNCE_MILLISECONDS = 500; + + /// + /// Gets or sets the expectations, values, and goals of the user, in their own words. + /// + [Parameter] + public string Expectations { get; set; } = string.Empty; + + [Parameter] + public EventCallback ExpectationsChanged { get; set; } + + /// + /// Gets or sets whether the user confirmed that the judge may decide by its own standards. It counts only while + /// there are no expectations. + /// + [Parameter] + public bool OwnStandardsAccepted { get; set; } + + [Parameter] + public EventCallback OwnStandardsAcceptedChanged { get; set; } + + private MudTextField? expectationsField; + + /// + /// Whether there were expectations the last time the parameters arrived, i.e., whether the switch was hidden; null + /// before they arrived the first time. + /// + private bool? hadExpectations; + + /// + /// Whether the switch has just gone away, so that the form has to forget what it found fault with in it. + /// + private bool switchWentAway; + + #region Overrides of ComponentBase + + protected override void OnParametersSet() + { + if (this.hadExpectations is false && this.HasExpectations) + this.switchWentAway = true; + + this.hadExpectations = this.HasExpectations; + base.OnParametersSet(); + } + + /// + /// A MudForm gathers the issues of its fields only when one of them is checked. Without the switch, its issue + /// would stay in the list of issues until the next check. Checking the field of the expectations after the render + /// which removed the switch lets the form gather the issues anew, from the fields which are still there. + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (this.switchWentAway) + { + this.switchWentAway = false; + if (this.expectationsField is not null) + await this.expectationsField.ValidateAsync(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + /// + /// Checks whether the judge may start: with expectations of the user, or with the confirmation that it may decide + /// by its own standards. + /// + /// The expectations, values, and goals of the user. + /// Whether the user confirmed that the judge may decide by its own standards. + /// What is missing, or null when the judge may start. + public static string? IssueOf(string expectations, bool ownStandardsAccepted) => string.IsNullOrWhiteSpace(expectations) && !ownStandardsAccepted + ? TA("Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right.") + : null; + + private bool HasExpectations => !string.IsNullOrWhiteSpace(this.Expectations); + + private Task OnExpectationsChanged(string? text) => this.ExpectationsChanged.InvokeAsync(text ?? string.Empty); + + /// + /// The form shows the issue of a field the user has not touched only once it checks all of them, i.e., when the + /// user starts. So the check needs no flag of its own for that, unlike a field whose value only arrives later. + /// + private string? ValidatingAcceptance(bool accepted) => IssueOf(this.Expectations, accepted); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeFormat.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeFormat.cs new file mode 100644 index 000000000..ec0b00fec --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeFormat.cs @@ -0,0 +1,85 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How a judge of the Model Comparison assistant names the two things it compares, and how it answers: the same JSON +/// for every judge, so that ModelComparisonJudgeVerdict reads them all. +/// +/// +/// The judge of the stability mode compares two answers, "A" and "B", the columns the user sees. The judge of a +/// comparison series compares two chats, "1" and "2", the names the user sees. Either way, the judge refers to them by +/// these names only, so that its reasoning matches the screen, and never by a model. +/// +/// What the judge compares, in the singular, e.g., "answer". +/// What kind of name tells the two apart, e.g., "letter". +/// The name of the first one, which stands for COLUMN_A. +/// The name of the second one, which stands for COLUMN_B. +public sealed record ModelComparisonJudgeFormat(string Subject, string NameKind, string First, string Second) +{ + /// + /// Two answers, "A" and "B", as in the stability mode. + /// + public static readonly ModelComparisonJudgeFormat ANSWERS = new("answer", "letter", "A", "B"); + + /// + /// Two chats, "1" and "2", as in a comparison series. + /// + public static readonly ModelComparisonJudgeFormat CHATS = new("chat", "number", "1", "2"); + + /// + /// Tells the judge how to answer. + /// + /// The language of the reasoning, by its English name, or an empty text to leave it to the judge. + /// The instructions, as the last part of a prompt. + public string AnswerInstructions(string languageName) => $$""" + Respond with exactly one JSON object and nothing else, in this shape: + + {"preferred": "{{this.First}}", "reasoning": "A short explanation, a few sentences at most."} + + The value of "preferred" must be exactly one of "{{this.First}}", "{{this.Second}}", or "TIE". In the reasoning, refer to each {{this.Subject}} only by its {{this.NameKind}}, {{this.First}} or {{this.Second}}, never by a model name -- translated into whatever language you write the reasoning in, not forced into English.{{this.LanguageInstruction(languageName)}} + """; + + /// + /// Reads which one the judge preferred. + /// + /// The value of "preferred" as the judge wrote it. + /// COLUMN_A for the first one, COLUMN_B for the second one, TIE, or null for anything else. + public ModelComparisonVote? VoteOf(string? preferred) + { + var value = preferred?.Trim().ToUpperInvariant(); + if (value == this.First.ToUpperInvariant()) + return ModelComparisonVote.COLUMN_A; + + if (value == this.Second.ToUpperInvariant()) + return ModelComparisonVote.COLUMN_B; + + return value == "TIE" ? ModelComparisonVote.TIE : null; + } + + /// + /// Tells the judge to decide by the expectations, values, and goals of the user, see + /// ModelComparisonJudgeExpectations: without them, the judge would decide by what it learned in training. + /// + /// The expectations, values, and goals of the user, or an empty text when the user gave none. + /// A paragraph and a section of their own, to follow the criterion of the judge, or an empty text. + public static string ExpectationsOfTheUser(string expectations) => expectations.Trim().Length == 0 + ? string.Empty + : $"\n\nDecide by the expectations, values, and goals of the user below. They decide what counts as good here, before any preference of your own.\n\n# Expectations, values, and goals of the user\n{expectations.Trim()}"; + + /// + /// Adds an identifier to a prompt of a judge as a plainly labeled, ignorable line, so that a caching gateway does + /// not answer a judge which is asked again about similar content from its cache, see ModelComparisonRequest.ToPrompt. + /// + /// The prompt. + /// The identifier, or an empty text to leave the prompt as it is. + /// The prompt. + public static string WithCacheBuster(string prompt, string cacheBuster) => cacheBuster.Length == 0 + ? prompt + : $"{prompt}\n\n(Request ID: {cacheBuster}. This has no bearing on your verdict; it is only here so repeated requests are not answered from a cache.)"; + + /// + /// Tells the judge which language to write in, or nothing when no language was given. + /// + private string LanguageInstruction(string languageName) => languageName.Trim().Length == 0 + ? string.Empty + : $" Write the reasoning in {languageName.Trim()}, regardless of the language of the {this.Subject}s."; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor new file mode 100644 index 000000000..7be9f983f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor @@ -0,0 +1,8 @@ +@inherits MSGComponentBase + +@foreach (var paragraph in this.paragraphs) +{ + + @paragraph + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor.cs new file mode 100644 index 000000000..65541139a --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeReasoning.razor.cs @@ -0,0 +1,46 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Why the judge chose so, in justified paragraphs with hyphenation. +/// +/// +/// The judge writes its line breaks itself, and they belong to it. MudJustifiedText sets the style of its own, so +/// that it cannot keep line breaks with white-space: pre-wrap; every line of the reasoning becomes a paragraph instead. +/// +public partial class ModelComparisonJudgeReasoning : MSGComponentBase +{ + /// + /// Gets or sets the reasoning of the judge. + /// + [Parameter, EditorRequired] + public string Reasoning { get; set; } = string.Empty; + + /// + /// The paragraphs of the reasoning shown, without empty lines. + /// + private string[] paragraphs = []; + + /// + /// The reasoning which paragraphs was built from. + /// + private string? shownReasoning; + + #region Overrides of ComponentBase + + protected override void OnParametersSet() + { + if (!string.Equals(this.shownReasoning, this.Reasoning, StringComparison.Ordinal)) + { + this.shownReasoning = this.Reasoning; + this.paragraphs = this.Reasoning.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + base.OnParametersSet(); + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeRequest.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeRequest.cs new file mode 100644 index 000000000..429505e22 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeRequest.cs @@ -0,0 +1,87 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the judge is asked: the same request the two models received, their two answers, and +/// the expectations, values, and goals of the user, when the user described them. +/// +/// +/// The judge is asked once both models have answered, and is kept as blind to which model wrote +/// which answer as the user is: the answers go in as plain text, labeled "Answer A" and "Answer B", +/// never by model name. These are the same letters the user sees, in the same order, so the judge's +/// own words about "Answer A"/"Answer B" line up with what is on screen -- the caller has to settle +/// the presentation order before building this, not after. ModelComparisonVote is what +/// the judge answers with, the same column terms the user's own vote uses. +/// +public sealed record ModelComparisonJudgeRequest +{ + /// The request the two models were asked. + /// + /// The expectations, values, and goals of the user, in their own words, by which the judge + /// decides, see ModelComparisonJudgeExpectations; or an empty text, when the user confirmed that + /// the judge may decide by its own standards with nothing but the generic criterion: which answer + /// better answers the question or completes the task. + /// + /// The answer standing in column A. + /// The answer standing in column B. + /// + /// The language the judge is asked to write its reasoning in, by its English name (for example + /// "German"), or an empty text to leave the language to the judge itself. + /// + public ModelComparisonJudgeRequest(ModelComparisonRequest request, string instructions, string answerA, string answerB, string languageName = "") + { + this.Request = request; + this.Instructions = instructions.Trim(); + this.AnswerA = answerA; + this.AnswerB = answerB; + this.LanguageName = languageName.Trim(); + } + + public ModelComparisonRequest Request { get; } + + public string Instructions { get; } + + public string AnswerA { get; } + + public string AnswerB { get; } + + public string LanguageName { get; } + + /// + /// What the judge is told to look for: the generic criterion, and the expectations, values, and + /// goals of the user, when the user described them, which come first. + /// + private string Criterion => "Judge which answer better answers the question or better completes the task in the request below." + ModelComparisonJudgeFormat.ExpectationsOfTheUser(this.Instructions); + + /// + /// Builds the prompt for the judge model, asking for one JSON object and nothing else. + /// + /// + /// An identifier to append as a plainly labeled, ignorable line, or an empty text to leave the + /// prompt exactly as asked. See ModelComparisonRequest.ToPrompt for why: the judge + /// is asked fresh for every run of a batch, same as the two models, and a caching gateway cannot + /// tell that apart from one identical request repeated unless the prompt says so itself -- most + /// of all here, where two similar answers can easily make for a near-identical judge prompt too. + /// + /// The user prompt the judge model receives. + public string ToPrompt(string cacheBuster = "") + { + var prompt = $$""" + You judge two AI answers to the same request. You do not know, and must not guess, which model wrote which answer, so judge only by what the answers say. + + {{this.Criterion}} + + # Request + {{this.Request.ToPrompt(headingLevel: 2)}} + + # Answer A + {{this.AnswerA}} + + # Answer B + {{this.AnswerB}} + + Decide which answer is better, or whether they are equally good. {{ModelComparisonJudgeFormat.ANSWERS.AnswerInstructions(this.LanguageName)}} + """; + + return ModelComparisonJudgeFormat.WithCacheBuster(prompt, cacheBuster); + } +} diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdict.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdict.cs new file mode 100644 index 000000000..1e0d480a6 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdict.cs @@ -0,0 +1,98 @@ +using System.Text.Json; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the judge said about the two answers, once it could be understood. +/// +/// +/// Built from whatever the judge model answered, the same way ModelComparisonAnswer +/// is built from what the two compared models answered: a judge which did not answer, or whose +/// answer could not be read as the JSON it was asked for, ends up with Completed +/// false and nothing else to show. +/// +public sealed record ModelComparisonJudgeVerdict +{ + /// + /// The name to show for the judge model. Shown together with its verdict, once the user has + /// voted -- the same point at which the names of the two compared models are revealed. + /// + public required string Label { get; init; } + + /// + /// Whether the judge could be understood: it answered, and that answer held a preference in the + /// expected shape. + /// + public required bool Completed { get; init; } + + /// + /// Which column the judge preferred, or null when Completed is false. The same + /// column terms the user's own vote uses, since the judge is asked about "Answer A"/"Answer B", + /// never about a model. + /// + public ModelComparisonVote? Preferred { get; init; } + + /// + /// Why the judge decided as it did, or an empty text when Completed is false. + /// + public string Reasoning { get; init; } = string.Empty; + + /// + /// How the JSON of the judge is read: as forgiving as models write it, with comments and trailing commas. + /// + internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + TypeInfoResolver = ModelComparisonJsonContext.Default, + }.AsReadOnly(); + + /// + /// Reads the judge's answer. Never throws: whatever cannot be understood becomes a verdict + /// which is not Completed, the same as a judge which did not answer at all. + /// + /// The name of the judge model. + /// Whether the judge model answered at all, before its text is even looked at. + /// What the judge model answered. + public static ModelComparisonJudgeVerdict Parse(string label, bool judgeAnswered, string judgeText) => Parse(label, judgeAnswered, judgeText, ModelComparisonJudgeFormat.ANSWERS); + + /// + /// Reads the judge's answer about the two things a format names, e.g., two chats. Never throws, see the other + /// overload. + /// + /// The name of the judge model. + /// Whether the judge model answered at all, before its text is even looked at. + /// What the judge model answered. + /// How the judge was asked to name the two, see ModelComparisonJudgeFormat. + public static ModelComparisonJudgeVerdict Parse(string label, bool judgeAnswered, string judgeText, ModelComparisonJudgeFormat format) + { + var notCompleted = new ModelComparisonJudgeVerdict { Label = label, Completed = false }; + if (!judgeAnswered) + return notCompleted; + + var json = ModelAnswerJson.ExtractObject(judgeText); + if (json.IsWhiteSpace()) + return notCompleted; + + try + { + var parsed = JsonSerializer.Deserialize(json, JSON_OPTIONS); + if (parsed is null || format.VoteOf(parsed.Preferred) is not { } preference) + return notCompleted; + + return new ModelComparisonJudgeVerdict + { + Label = label, + Completed = true, + Preferred = preference, + Reasoning = parsed.Reasoning?.Trim() ?? string.Empty, + }; + } + catch (JsonException) + { + return notCompleted; + } + } +} diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor new file mode 100644 index 000000000..3ccbd39dd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor @@ -0,0 +1,26 @@ +@inherits MSGComponentBase + +@if (this.Result.Judge is { } judge) +{ + + + + + @string.Format(ModelComparisonFormat.Culture, T("Judge: {0}"), judge.Label) + + + @if (judge.Completed) + { + + @this.PreferenceText + + + } + else + { + + @T("The judge did not give a usable verdict.") + + } + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor.cs new file mode 100644 index 000000000..d7d999d6c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonJudgeVerdictCard.razor.cs @@ -0,0 +1,60 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the judge said about the two answers of a run, with the names of the models. Shown only in the summary of a +/// batch, so it cannot anchor a vote of the user; without a judge, it shows nothing. +/// +public partial class ModelComparisonJudgeVerdictCard : MSGComponentBase +{ + /// + /// Gets or sets the run, with the verdict of the judge. + /// + [Parameter, EditorRequired] + public ModelComparisonRunResult Result { get; set; } = null!; + + /// + /// Gets or sets the model the user preferred in the run, or null without a vote. + /// + [Parameter] + public ModelComparisonModelChoice? VoteChoice { get; set; } + + /// + /// Gets or sets the model the judge preferred in the run, or null without a usable verdict. + /// + [Parameter] + public ModelComparisonModelChoice? JudgeChoice { get; set; } + + /// + /// Names what the judge preferred, by the name of the model, and says whether that matches the vote of the user. + /// + private string PreferenceText + { + get + { + if (this.JudgeChoice is not { } preferred) + return string.Empty; + + if (preferred is ModelComparisonModelChoice.TIE) + return this.VoteChoice switch + { + null => T("The judge saw both answers as equally good."), + ModelComparisonModelChoice.TIE => TA("The judge saw both answers as equally good, matching your own vote."), + + _ => TA("The judge saw both answers as equally good, while you preferred one of them."), + }; + + var preferredName = this.Result.AnswerOf(preferred).Label; + return this.VoteChoice switch + { + null => string.Format(ModelComparisonFormat.Culture, T("The judge preferred {0}."), preferredName), + { } chosen when chosen == preferred => string.Format(ModelComparisonFormat.Culture, TA("The judge preferred {0}, matching your own vote."), preferredName), + + _ => string.Format(ModelComparisonFormat.Culture, TA("The judge preferred {0}, differing from your own vote."), preferredName), + }; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonMode.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonMode.cs new file mode 100644 index 000000000..ada45d333 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonMode.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the Model Comparison assistant finds out about two models. +/// +public enum ModelComparisonMode +{ + /// + /// How consistently the models answer one request, sent once or several times in a row. + /// + STABILITY, + + /// + /// Which model helps the user more with real tasks, each worked on in a chat with both models, one after the other. + /// + TASKS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor new file mode 100644 index 000000000..9f2327476 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor @@ -0,0 +1,22 @@ +@attribute [Route(Routes.ASSISTANT_MODEL_COMPARISON)] +@inherits MSGComponentBase + +
+ + + @T("Model Comparison") + + + + + + + + + + + @TA("Compare two models without knowing which one answers. Choose what you want to find out.") + + + +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor.cs new file mode 100644 index 000000000..1004be7e4 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeChoice.razor.cs @@ -0,0 +1,54 @@ +using AIStudio.Components; +using AIStudio.Dialogs; +using AIStudio.Dialogs.Settings; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The start page of the Model Comparison assistant, where the user chooses between its two modes. +/// +/// +/// The tile of the assistant leads here. Each mode has a page of its own, so that the assistant session of the +/// stability mode survives a visit to the other mode, and a link or the browser history may lead to either directly. +/// +public partial class ModelComparisonModeChoice : MSGComponentBase +{ + [Inject] + private NavigationManager NavigationManager { get; init; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + if (!ModelComparisonAccess.CanOpen(this.SettingsManager, T("Model Comparison"))) + this.NavigationManager.NavigateTo(Routes.ASSISTANTS); + } + + #endregion + + private IReadOnlyList> Options => + [ + new(ModelComparisonMode.STABILITY, ModelComparisonMode.STABILITY.Icon, ModelComparisonMode.STABILITY.Name, TA("Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer.")), + new(ModelComparisonMode.TASKS, ModelComparisonMode.TASKS.Icon, ModelComparisonMode.TASKS.Name, TA("Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks.")), + ]; + + private void ChooseMode(ModelComparisonMode mode) => this.NavigationManager.NavigateTo(mode.Route); + + /// + /// Shows what all stored comparisons say about the models, as both modes do. + /// + private async Task OpenResultsDialog() => await this.DialogService.ShowAsync(T("Results of all comparisons"), DialogOptions.FULLSCREEN); + + /// + /// Opens the options of the assistant, which both modes share. + /// + private async Task OpenSettingsDialog() => await this.DialogService.ShowAsync(null, new DialogParameters(), DialogOptions.FULLSCREEN); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeExtensions.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeExtensions.cs new file mode 100644 index 000000000..240198163 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModeExtensions.cs @@ -0,0 +1,45 @@ +namespace AIStudio.Assistants.ModelComparison; + +public static class ModelComparisonModeExtensions +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ModelComparisonModeExtensions).Namespace, nameof(ModelComparisonModeExtensions)); + + /// The mode of the Model Comparison assistant. + extension(ModelComparisonMode mode) + { + /// + /// The short name of the mode, which tells the user in which of the two modes they are. + /// + public string Name => mode switch + { + ModelComparisonMode.TASKS => TB("Everyday Tasks"), + + _ => TB("Answer Variance"), + }; + + /// + /// The title of the page of the mode, which names the assistant together with the mode. + /// + public string PageTitle => string.Format(TB("Model Comparison: {0}"), mode.Name); + + /// + /// The page of the mode. + /// + public string Route => mode switch + { + ModelComparisonMode.TASKS => Routes.ASSISTANT_MODEL_COMPARISON_TASKS, + + _ => Routes.ASSISTANT_MODEL_COMPARISON_STABILITY, + }; + + /// + /// The icon of the mode. + /// + public string Icon => mode switch + { + ModelComparisonMode.TASKS => AppIcons.COMPARISON_TASKS, + + _ => AppIcons.COMPARISON_STABILITY, + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelChoice.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelChoice.cs new file mode 100644 index 000000000..e515d3c4c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelChoice.cs @@ -0,0 +1,17 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Which model a column-based choice -- a vote or a judge verdict -- actually means, once translated +/// out of the run's own presentation order. +/// +/// +/// First/Second names the models the way the user picked them, the one thing that stays comparable +/// across every run of a batch: the column a model stood in is drawn fresh for every run, so a tally +/// kept in column terms would count nothing meaningful. +/// +public enum ModelComparisonModelChoice +{ + FIRST, + SECOND, + TIE, +} diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelIdentity.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelIdentity.cs new file mode 100644 index 000000000..6371e445b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelIdentity.cs @@ -0,0 +1,61 @@ +using System.Text.Json.Serialization; + +using AIStudio.Provider; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Which model took part in a stored comparison. +/// +/// +/// The statistics count a model across comparisons by its provider, its host, and its ID, not by the provider entry +/// of the settings: the user may delete or rename that entry, and two entries may point to the same model. Only a +/// self-hosted model has a host of its own; two servers may run models of the same name which are not the same. +/// +public sealed record ModelComparisonModelIdentity +{ + /// + /// The kind of provider which ran the model. + /// + public LLMProviders LLMProvider { get; set; } = LLMProviders.NONE; + + /// + /// The address of the server of a self-hosted model; empty for every other provider. + /// + public string Hostname { get; set; } = string.Empty; + + /// + /// The ID of the model at its provider. + /// + public string ModelId { get; set; } = string.Empty; + + /// + /// The name the comparison showed for the model, for the statistics to show it again. + /// + public string Label { get; set; } = string.Empty; + + /// + /// Gets the identity of the model of a provider entry. + /// + /// The provider entry, with its model. + /// The identity. + public static ModelComparisonModelIdentity Of(AIStudio.Settings.Provider provider) => new() + { + LLMProvider = provider.UsedLLMProvider, + Hostname = provider.IsSelfHosted ? provider.Hostname : string.Empty, + ModelId = provider.Model.Id, + Label = provider.ToString(), + }; + + /// + /// Gets what tells this model apart from others, whatever the label. + /// + [JsonIgnore] + public ModelComparisonModelKey Key => ModelComparisonModelKey.Of(this.LLMProvider, this.Hostname, this.ModelId); + + /// + /// Whether both identities name the same model, whatever the label. + /// + /// The other identity. + public bool IsSameModelAs(ModelComparisonModelIdentity other) => this.Key == other.Key; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelKey.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelKey.cs new file mode 100644 index 000000000..70561d95a --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelKey.cs @@ -0,0 +1,35 @@ +using AIStudio.Provider; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What tells one model apart from another across stored comparisons, see ModelComparisonModelIdentity. +/// +/// The kind of provider which ran the model. +/// The address of the server of a self-hosted model in lowercase, without a trailing slash; empty for every other provider. +/// The ID of the model at its provider. +public readonly record struct ModelComparisonModelKey(LLMProviders LLMProvider, string Hostname, string ModelId) : IComparable +{ + /// + /// Gets the key of a model, with its host written the same way however the user typed it. + /// + /// The kind of provider which ran the model. + /// The address of the server of a self-hosted model, or empty. + /// The ID of the model at its provider. + /// The key. + public static ModelComparisonModelKey Of(LLMProviders llmProvider, string hostname, string modelId) => new(llmProvider, hostname.Trim().TrimEnd('/').ToLowerInvariant(), modelId); + + /// + /// Orders keys the same way on every computer, so that a pair of models is always written in the same order. + /// + /// The other key. + public int CompareTo(ModelComparisonModelKey other) + { + var byProvider = this.LLMProvider.CompareTo(other.LLMProvider); + if (byProvider != 0) + return byProvider; + + var byHost = string.CompareOrdinal(this.Hostname, other.Hostname); + return byHost != 0 ? byHost : string.CompareOrdinal(this.ModelId, other.ModelId); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelSpeed.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelSpeed.cs new file mode 100644 index 000000000..a969b271c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelSpeed.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How fast a model answered across all stored comparisons. +/// +/// +/// Medians rather than means: a single answer which waited a minute for a busy server would otherwise shift the whole +/// picture. The times depend on the length of the answers and the load of the server as much as on the model, so they +/// compare models only when both ran on the same tasks. +/// +public sealed record ModelComparisonModelSpeed +{ + /// + /// The model, with the name it had in its latest comparison. + /// + public required ModelComparisonModelIdentity Model { get; init; } + + /// + /// How many complete answers the times come from. + /// + public int AnswerCount { get; init; } + + /// + /// The median time until the first piece of an answer arrived, or null without any. + /// + public TimeSpan? MedianFirstTokenTime { get; init; } + + /// + /// The median time until an answer was complete, or null without any. + /// + public TimeSpan? MedianTotalTime { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelStanding.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelStanding.cs new file mode 100644 index 000000000..14e86cbfe --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonModelStanding.cs @@ -0,0 +1,33 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How a model did across all stored comparisons against any other model: by the votes of the user, or by the +/// verdicts of a judge. +/// +public sealed record ModelComparisonModelStanding +{ + /// + /// The model, with the name it had in its latest comparison. + /// + public required ModelComparisonModelIdentity Model { get; init; } + + /// + /// How many runs the model won. + /// + public int Wins { get; init; } + + /// + /// How many runs the model lost. + /// + public int Losses { get; init; } + + /// + /// How many runs ended in a tie. + /// + public int Ties { get; init; } + + /// + /// The share of the decided runs which the model won, with how sure that share is. + /// + public ModelComparisonWinShare WinShare => ModelComparisonWinShare.Of(this.Wins, this.Losses); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonNames.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonNames.cs new file mode 100644 index 000000000..9c02feb5c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonNames.cs @@ -0,0 +1,83 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Keeps the names of presets and of comparison series unique, so that the user can tell them apart in their lists. +/// +/// +/// Two names count as the same regardless of case and of spaces around them, as the user reads them. +/// +public static partial class ModelComparisonNames +{ + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(ModelComparisonNames).Namespace, nameof(ModelComparisonNames)); + + /// + /// How long the name of a comparison series may be, so that it fits the list of the series. + /// + public const int MAX_SERIES_NAME_LENGTH = 60; + + /// + /// Checks the name of a comparison series, for a new series as well as for renaming one: it is needed, it fits, + /// and no other series has it. + /// + /// The name. + /// The names of the other series, without the one being renamed. + /// Why the name does not work, or null when it does. + public static string? SeriesNameIssue(string? name, IEnumerable takenNames) + { + if (string.IsNullOrWhiteSpace(name)) + return TA("Please give the series a name, so that you find it again."); + + if (name.Trim().Length > MAX_SERIES_NAME_LENGTH) + return string.Format(CultureInfo.InvariantCulture, TA("The name of your series may have at most {0} characters."), MAX_SERIES_NAME_LENGTH); + + if (IsTaken(name, takenNames)) + return TA("A series with this name already exists. Please choose a different name."); + + return null; + } + + /// + /// Tells whether a name is taken already. + /// + /// The name. + /// The names taken by the others, without the one the name belongs to. + /// True when another one has the name already. + public static bool IsTaken(string name, IEnumerable takenNames) + { + var trimmedName = name.Trim(); + return takenNames.Any(takenName => string.Equals(takenName.Trim(), trimmedName, StringComparison.CurrentCultureIgnoreCase)); + } + + /// + /// Gets a name which is not taken yet, based on a given one: the name itself when it is free, otherwise the name + /// with the next free number, e.g., "Weekly reports (2)". + /// + /// The name, e.g., of the series whose setup a new one takes over. + /// The names taken already. + /// How long the name may be; the base name is shortened for the number when needed. + /// The free name. + public static string FreeNameFrom(string name, IReadOnlyCollection takenNames, int maxLength) + { + var trimmedName = name.Trim(); + if (!IsTaken(trimmedName, takenNames) && trimmedName.Length <= maxLength) + return trimmedName; + + // A name with a number already counts on from its base, so that a copy of "Reports (2)" becomes "Reports (3)": + var match = NumberedName().Match(trimmedName); + var baseName = match.Success ? match.Groups["base"].Value : trimmedName; + for (var number = 2;; number++) + { + var suffix = string.Format(CultureInfo.InvariantCulture, " ({0})", number); + var shortenedBase = baseName.Length + suffix.Length > maxLength ? baseName[..Math.Max(0, maxLength - suffix.Length)].TrimEnd() : baseName; + var candidate = shortenedBase + suffix; + if (!IsTaken(candidate, takenNames)) + return candidate; + } + } + + [GeneratedRegex(@"^(?.+?) \((?\d+)\)$")] + private static partial Regex NumberedName(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonOutcome.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonOutcome.cs new file mode 100644 index 000000000..6d3c50fdd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonOutcome.cs @@ -0,0 +1,81 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How the summary of a batch shows what the user and the judge chose in a run. +/// +public static class ModelComparisonOutcome +{ + /// + /// Makes the card of an answer as high as its column, so that both cards of a run are equally high. + /// + public const string FILL_THE_COLUMN = "flex-grow-1"; + + /// + /// Gets how a model did in the vote of the user, as a color: green when its answer was preferred, red when the + /// other one was, orange when the user found them equally good. + /// + /// The model the user preferred in the run, or null without a vote. + /// The model whose outcome this is. + /// The color. + public static Color ColorOf(ModelComparisonModelChoice? voteChoice, ModelComparisonModelChoice model) => voteChoice switch + { + null => Color.Default, + ModelComparisonModelChoice.TIE => Color.Warning, + { } chosen when chosen == model => Color.Success, + + _ => Color.Error, + }; + + /// + /// Gets the icon which goes with ColorOf. The color alone would not tell the outcomes apart for readers who are + /// color blind, so every outcome has a shape of its own as well. + /// + /// The model the user preferred in the run, or null without a vote. + /// The model whose outcome this is. + /// The icon. + public static string IconOf(ModelComparisonModelChoice? voteChoice, ModelComparisonModelChoice model) => voteChoice switch + { + null => AppIcons.VOTE_MISSING, + ModelComparisonModelChoice.TIE => AppIcons.VOTE_TIE, + { } chosen when chosen == model => AppIcons.VOTE_WON, + + _ => AppIcons.VOTE_LOST, + }; + + /// + /// Gets the classes of the card around an answer: it fills the height of its column, and with a vote of the user, + /// it has a border in the color which goes with ColorOf. + /// + /// + /// The classes are the ones of MudBlazor: a border two pixels wide, in the color of the outcome. They override the + /// thin gray line of the card, which is why they are told to win. While the user votes, and for a run without a + /// vote, the answers stand in the plain frame of the chat. + /// + /// The model the user preferred in the run, or null without a vote. + /// The model whose answer the card shows. + /// The classes. + public static string CardClassOf(ModelComparisonModelChoice? voteChoice, ModelComparisonModelChoice model) + { + if (voteChoice is null) + return FILL_THE_COLUMN; + + var color = ColorOf(voteChoice, model) switch + { + Color.Success => "mud-border-success", + Color.Warning => "mud-border-warning", + + _ => "mud-border-error", + }; + + return $"{FILL_THE_COLUMN} border-2 border-solid {color}"; + } + + /// + /// Gets whether the badge of the judge belongs on the answer of a model: either the judge preferred it, or the judge + /// saw a tie, in which case both answers carry the badge. + /// + /// The model the judge preferred, or null without a usable verdict. + /// The model whose answer this is. + /// True when the answer carries the badge. + public static bool JudgeFavors(ModelComparisonModelChoice? judgeChoice, ModelComparisonModelChoice model) => judgeChoice is ModelComparisonModelChoice.TIE || judgeChoice == model; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor new file mode 100644 index 000000000..40d768fd6 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor @@ -0,0 +1,45 @@ +@inherits MSGComponentBase + + + + @string.Format(T("{0} vs. {1}"), this.Pairing.First.Label, this.Pairing.Second.Label) + + + @this.CaptionText + + + + + + + @this.Pairing.First.Label + @T("Equally good") + @this.Pairing.Second.Label + @T("Assessment") + + + + + @TA("Your votes") + @this.Pairing.Votes.FirstModelWins.CompactCount() + @this.Pairing.Votes.Ties.CompactCount() + @this.Pairing.Votes.SecondModelWins.CompactCount() + + + + + @if (this.Pairing.JudgeVerdicts.ScoredRunCount > 0) + { + + @T("Judge verdicts") + @this.Pairing.JudgeVerdicts.FirstModelWins.CompactCount() + @this.Pairing.JudgeVerdicts.Ties.CompactCount() + @this.Pairing.JudgeVerdicts.SecondModelWins.CompactCount() + + + + + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor.cs new file mode 100644 index 000000000..ea442507d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairCard.razor.cs @@ -0,0 +1,66 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How two models did against each other, by the votes of the user and, when there are any, by the verdicts of the +/// judges: as a chart, and as a table with the assessment. +/// +public partial class ModelComparisonPairCard : MSGComponentBase +{ + /// + /// Gets or sets the pair. + /// + [Parameter, EditorRequired] + public ModelComparisonPairing Pairing { get; set; } = null!; + + /// + /// Gets or sets what the line below the names says, or null for the number of stored comparisons. + /// + [Parameter] + public string? Caption { get; set; } + + /// + /// The tallies the chart shows: the votes of the user, and the verdicts of the judges when there are any. + /// + private IReadOnlyList Tallies => this.Pairing.JudgeVerdicts.ScoredRunCount > 0 + ? [this.Pairing.Votes, this.Pairing.JudgeVerdicts] + : [this.Pairing.Votes]; + + private string[] ChartLabels => this.Pairing.JudgeVerdicts.ScoredRunCount > 0 + ? [TA("Your votes"), T("Judge verdicts")] + : [TA("Your votes")]; + + /// + /// The wins of the first model, the ties, and the wins of the second model, as the three series of a stacked bar + /// chart with one bar per tally, in the order of the columns of the table. + /// + private List> OutcomeSeries => + [ + new() { Name = this.Pairing.First.Label, Data = new([.. this.Tallies.Select(tally => (double)tally.FirstModelWins)]) }, + new() { Name = T("Equally good"), Data = new([.. this.Tallies.Select(tally => (double)tally.Ties)]) }, + new() { Name = this.Pairing.Second.Label, Data = new([.. this.Tallies.Select(tally => (double)tally.SecondModelWins)]) }, + ]; + + private StackedBarChartOptions ChartOptions => new() + { + ChartPalette = ModelComparisonCharts.PAIR_PALETTE, + YAxisTicks = ModelComparisonCharts.AxisStep(this.Tallies.Select(tally => (double)(tally.FirstModelWins + tally.Ties + tally.SecondModelWins))), + YAxisToStringFunc = value => ((long)value).CompactCount(), + }; + + private string CaptionText => this.Caption ?? string.Format(ModelComparisonFormat.Culture, T("Stored comparisons: {0}"), this.Pairing.ComparisonCount.CompactCount()); + + /// + /// Says which model of the pair won clearly, if one did. + /// + /// The share of wins of the first model, by the votes or by the judges. + private string SignificanceText(ModelComparisonWinShare winShareOfFirst) => winShareOfFirst.Significance switch + { + ModelComparisonSignificance.CLEARLY_BETTER => string.Format(T("{0} is clearly better"), this.Pairing.First.Label), + ModelComparisonSignificance.CLEARLY_WORSE => string.Format(T("{0} is clearly better"), this.Pairing.Second.Label), + _ => T("Not yet clear"), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairKey.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairKey.cs new file mode 100644 index 000000000..c99083cd2 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairKey.cs @@ -0,0 +1,19 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Two models compared with each other, in the same order whichever of them was the first model of a comparison. +/// +/// The model which comes first by ModelComparisonModelKey.CompareTo. +/// The other model. +public readonly record struct ModelComparisonPairKey(ModelComparisonModelKey First, ModelComparisonModelKey Second) +{ + /// + /// Gets the pair of two models. + /// + /// One model. + /// The other model. + /// The pair, in its own order. + public static ModelComparisonPairKey Of(ModelComparisonModelKey one, ModelComparisonModelKey other) => one.CompareTo(other) <= 0 + ? new(one, other) + : new(other, one); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairing.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairing.cs new file mode 100644 index 000000000..03fde9a0c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPairing.cs @@ -0,0 +1,46 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How two models did directly against each other across all stored comparisons of the two. +/// +/// +/// The first and the second model are those of the pair, see ModelComparisonPairKey, not those of a single comparison: +/// a comparison in which the user picked them the other way round counts the same. +/// +public sealed record ModelComparisonPairing +{ + /// + /// The first model of the pair, with the name it had in its latest comparison. + /// + public required ModelComparisonModelIdentity First { get; init; } + + /// + /// The second model of the pair, with the name it had in its latest comparison. + /// + public required ModelComparisonModelIdentity Second { get; init; } + + /// + /// The votes of the user. + /// + public ModelComparisonTally Votes { get; init; } = ModelComparisonTally.NONE; + + /// + /// The verdicts of the judges. + /// + public ModelComparisonTally JudgeVerdicts { get; init; } = ModelComparisonTally.NONE; + + /// + /// How many comparisons of the two there are. + /// + public int ComparisonCount { get; init; } + + /// + /// The share of the decided votes which the first model won, with how sure that share is. + /// + public ModelComparisonWinShare WinShareOfFirstByVotes => ModelComparisonWinShare.Of(this.Votes.FirstModelWins, this.Votes.SecondModelWins); + + /// + /// The share of the decided verdicts of the judges which the first model won, with how sure that share is. + /// + public ModelComparisonWinShare WinShareOfFirstByJudges => ModelComparisonWinShare.Of(this.JudgeVerdicts.FirstModelWins, this.JudgeVerdicts.SecondModelWins); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonParticipant.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonParticipant.cs new file mode 100644 index 000000000..2d2e5d2d4 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonParticipant.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// One of the two models in a comparison, and the way to ask it. +/// +/// +/// The way to ask is a function rather than a provider, so the comparison itself does not depend on +/// anything of the app. ModelComparisonProviderAdapter builds the function for a real +/// provider. +/// +public sealed record ModelComparisonParticipant +{ + /// + /// The name to show for the model. It is copied into the answer, see ModelComparisonAnswer.Label. + /// + public required string Label { get; init; } + + /// + /// Sends a prompt to the model and streams the answer back, chunk by chunk. + /// + public required Func> Ask { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreselection.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreselection.cs new file mode 100644 index 000000000..0dfc6dde0 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreselection.cs @@ -0,0 +1,75 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Decides which models a new preset or a new comparison series of the Model Comparison assistant starts with. +/// +/// +/// SettingsManager.GetPreselectedProvider cannot decide this: it always falls back to the provider of the whole +/// app, while the options of this assistant may also preselect no model at all, e.g., because the user wants to +/// pick both models anew for each comparison. +/// +public static class ModelComparisonPreselection +{ + /// The settings. + extension(SettingsManager settingsManager) + { + /// + /// Gets the first model of a new preset or comparison series: the preselected model of the options, or the + /// provider of the whole app without them. + /// + /// + /// A provider which the organization prohibited, or which misses the confidence requirements of the + /// assistant, preselects no model. + /// + public AIStudio.Settings.Provider PreselectedComparisonModel + { + get + { + var options = settingsManager.ConfigurationData.ModelComparison; + return options.PreselectOptions + ? Resolve(settingsManager, options.PreselectedProvider) + : settingsManager.GetPreselectedProvider(Tools.Components.MODEL_COMPARISON_ASSISTANT); + } + } + + /// + /// Gets the judge of a new preset or comparison series: the preselected judge model of the options, or no + /// model without them. A new preset or comparison series switches its judge on only when this is a model. + /// + /// + /// A provider which the organization prohibited, or which misses the confidence requirements of the + /// assistant, preselects no model. + /// + public AIStudio.Settings.Provider PreselectedComparisonJudge + { + get + { + var options = settingsManager.ConfigurationData.ModelComparison; + return options.PreselectOptions + ? Resolve(settingsManager, options.PreselectedJudgeProvider) + : AIStudio.Settings.Provider.NONE; + } + } + } + + /// + /// Resolves the value of a model option into a provider which the assistant may use. + /// + /// The settings. + /// The value of the option: DataModelComparison.NO_MODEL, an empty text for the + /// provider of the whole app, or the ID of a provider. + /// The provider, or NONE when the option preselects no model or names one the assistant may not use. + private static AIStudio.Settings.Provider Resolve(SettingsManager settingsManager, string providerId) + { + if (string.Equals(providerId, DataModelComparison.NO_MODEL, StringComparison.OrdinalIgnoreCase)) + return AIStudio.Settings.Provider.NONE; + + var provider = settingsManager.GetProviderById(string.IsNullOrWhiteSpace(providerId) ? settingsManager.ConfigurationData.App.PreselectedProvider : providerId); + return settingsManager.IsProviderConfident(provider, Tools.Components.MODEL_COMPARISON_ASSISTANT) + ? provider + : AIStudio.Settings.Provider.NONE; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrder.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrder.cs new file mode 100644 index 000000000..79354be32 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrder.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Which of the two models is shown first, that is, in column A. +/// +/// +/// The order is drawn at random for every comparison, so that the position of an answer says +/// nothing about the model. The first model is the one the user picked first. +/// +public enum ModelComparisonPresentationOrder +{ + FIRST_MODEL_FIRST, + SECOND_MODEL_FIRST, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrderExtensions.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrderExtensions.cs new file mode 100644 index 000000000..646667bcd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresentationOrderExtensions.cs @@ -0,0 +1,48 @@ +namespace AIStudio.Assistants.ModelComparison; + +public static class ModelComparisonPresentationOrderExtensions +{ + /// Which model is shown in column A. + extension(ModelComparisonPresentationOrder order) + { + /// + /// Gets the answer which is shown in column A. + /// + /// The answers of both models. + /// The answer for column A. + public ModelComparisonAnswer InColumnA(ModelComparisonRunResult run) => order switch + { + ModelComparisonPresentationOrder.FIRST_MODEL_FIRST => run.First, + ModelComparisonPresentationOrder.SECOND_MODEL_FIRST => run.Second, + + _ => throw new ArgumentOutOfRangeException(nameof(order), order, $"The presentation order '{order}' is not known."), + }; + + /// + /// Gets the answer which is shown in column B. + /// + /// The answers of both models. + /// The answer for column B. + public ModelComparisonAnswer InColumnB(ModelComparisonRunResult run) => order switch + { + ModelComparisonPresentationOrder.FIRST_MODEL_FIRST => run.Second, + ModelComparisonPresentationOrder.SECOND_MODEL_FIRST => run.First, + + _ => throw new ArgumentOutOfRangeException(nameof(order), order, $"The presentation order '{order}' is not known."), + }; + + /// + /// Turns a column-based choice back into the model it means. The reverse of InColumnA and InColumnB: + /// those turn a known model into its column, this turns a known column back into its model. + /// + /// The column-based choice, from the user's own vote or from a judge verdict. + public ModelComparisonModelChoice ToModelChoice(ModelComparisonVote vote) => vote switch + { + ModelComparisonVote.TIE => ModelComparisonModelChoice.TIE, + ModelComparisonVote.COLUMN_A => order is ModelComparisonPresentationOrder.FIRST_MODEL_FIRST ? ModelComparisonModelChoice.FIRST : ModelComparisonModelChoice.SECOND, + ModelComparisonVote.COLUMN_B => order is ModelComparisonPresentationOrder.FIRST_MODEL_FIRST ? ModelComparisonModelChoice.SECOND : ModelComparisonModelChoice.FIRST, + + _ => throw new ArgumentOutOfRangeException(nameof(vote), vote, $"The vote '{vote}' is not known."), + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreset.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreset.cs new file mode 100644 index 000000000..c35ecfd18 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPreset.cs @@ -0,0 +1,52 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// A saved set of the inputs of the Model Comparison assistant, so that a familiar test case does not have to be typed +/// and picked by hand again every time. +/// +/// +/// Each preset is a file of its own below the data directory, see ModelComparisonStore, not a part of the settings: a +/// preset can carry a long document as its context, and the settings are written anew whenever any setting changes. +/// Presets belong to the user alone; no configuration plugin rolls them out. +/// +public sealed record ModelComparisonPreset +{ + /// + /// The ID of the preset, which names its file. + /// + public Guid Id { get; set; } + + /// + /// The name shown in the preset picker. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The ID of the provider of the first model, or empty when none was selected while saving. Resolved back to a + /// provider through SettingsManager.GetProviderById, which answers Provider.NONE for an ID that no longer exists. + /// + public string FirstProviderId { get; set; } = string.Empty; + + /// + /// The ID of the provider of the second model, see FirstProviderId. + /// + public string SecondProviderId { get; set; } = string.Empty; + + public bool JudgeEnabled { get; set; } + + /// + /// The ID of the provider of the judge, see FirstProviderId. + /// + public string JudgeProviderId { get; set; } = string.Empty; + + /// + /// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations. + /// + public string JudgeInstructions { get; set; } = string.Empty; + + public string Context { get; set; } = string.Empty; + + public string Question { get; set; } = string.Empty; + + public int RunCount { get; set; } = 1; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresetChoice.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresetChoice.cs new file mode 100644 index 000000000..2f6b9dcbe --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonPresetChoice.cs @@ -0,0 +1,30 @@ +using AIStudio.Settings.DataModel; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Decides which preset the mode Answer Variance works in, since it always works in one. +/// +public static class ModelComparisonPresetChoice +{ + /// + /// Chooses the preset the mode Answer Variance opens with, and the one it continues with after a preset was deleted: the + /// preselected preset of the options, or else the first one. + /// + /// + /// An assistant session which the page restores brings back the preset of its own instead, because the form it + /// restores belongs to that preset. + /// + /// The presets, in the order the list shows them. + /// The options of the assistant. + /// The ID of the preset, or null when there is none, and a new one is needed. + public static Guid? Choose(IReadOnlyList presets, DataModelComparison options) + { + if (options.PreselectOptions + && Guid.TryParse(options.PreselectedPresetId, out var preselectedId) + && presets.Any(preset => preset.Id == preselectedId)) + return preselectedId; + + return presets.Count > 0 ? presets[0].Id : null; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderAdapter.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderAdapter.cs new file mode 100644 index 000000000..35a8251ba --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderAdapter.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; + +using AIStudio.Chat; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Tools.Security; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Connects a provider of the app to the model comparison. +/// +public static class ModelComparisonProviderAdapter +{ + /// + /// Creates the participant for a provider. + /// + /// The provider, with the model to ask. + /// The settings the provider needs for its request. + /// The component whose confidence requirements the provider has to meet. + /// The title of the assistant, as the name of the throwaway chat. + /// The participant. + /// + /// The provider is built once here, not once per run: it holds nothing but its own + /// HttpClient, so every run of a batch reuses the same connection instead of paying for a + /// fresh one every time. + /// + public static ModelComparisonParticipant CreateParticipant(AIStudio.Settings.Provider providerSettings, SettingsManager settingsManager, Tools.Components component, string title) + { + var provider = providerSettings.CreateProvider(); + return new() + { + Label = providerSettings.ToString(), + Ask = (prompt, token) => StreamAnswer(provider, providerSettings, settingsManager, component, title, prompt, token), + }; + } + + /// + /// Creates the filter which the answers of both models pass before the judge reads them. + /// + /// The prompt injection filter of the app. + /// The batch the answers belong to, so the user hears once, when it is over, what the filter removed. + /// The title of the assistant, as the source the report names. + /// The filter, for ModelComparisonRunner. + public static Func, Task>> CreateJudgeInputFilter(PromptInjectionGuardService guard, PromptInjectionReportingScope reportingScope, string title) => + answers => guard.SanitizeAsync([.. answers.Select(answer => new PromptInjectionText(answer, PromptInjectionSource.ModelAnswer(title)))], reportingScope); + + /// + /// Asks the model straight through the provider, without ContentText.CreateFromProviderAsync. + /// + /// + /// Two reasons. The stream of chunks is what the first-token time is measured on, and the + /// streaming event of a ContentText only fires every few seconds in the energy saving + /// mode. And that method turns a provider which is not allowed into an empty answer without a + /// word, which would let the model lose every vote. Here it is an error, so the run reports it + /// as a failure of that model. + /// + /// The request is what the model comparison promises: no profile, no system prompt of its own, + /// no tools and no data sources, only the prompt. + /// + private static async IAsyncEnumerable StreamAnswer( + IProvider provider, + AIStudio.Settings.Provider providerSettings, + SettingsManager settingsManager, + Tools.Components component, + string title, + string prompt, + [EnumeratorCancellation] CancellationToken token) + { + // Fails for a provider the organization prohibited as well, see SettingsManager.IsProviderUsable: + if (!settingsManager.IsProviderConfident(providerSettings, component)) + throw new InvalidOperationException($"The provider '{providerSettings.InstanceName}' may not be used by the model comparison: the organization prohibited it, or it does not meet the confidence requirements."); + + var chatThread = new ChatThread + { + IncludeDateTime = false, + SelectedProvider = providerSettings.Id, + SelectedProfile = Profile.NO_PROFILE.Id, + SelectedToolIds = [], + SystemPrompt = string.Empty, + WorkspaceId = Guid.Empty, + ChatId = Guid.NewGuid(), + Name = title, + Blocks = [], + RuntimeComponent = component, + RuntimeSelectedToolIds = [], + RuntimeToolsAreAssistantManaged = true, + }; + + chatThread.Blocks.Add(new ContentBlock + { + Time = DateTimeOffset.Now, + ContentType = ContentType.TEXT, + Role = ChatRole.USER, + Content = new ContentText + { + Text = prompt, + }, + }); + + // The provider writes tool traces into the last block of the AI, so the thread has one: + chatThread.Blocks.Add(new ContentBlock + { + Time = DateTimeOffset.Now, + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = new ContentText(), + }); + + await foreach (var chunk in provider.StreamChatCompletion(providerSettings.Model, chatThread, settingsManager, token).ConfigureAwait(false)) + yield return chunk.Content; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderCheck.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderCheck.cs new file mode 100644 index 000000000..a3a4979c3 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonProviderCheck.cs @@ -0,0 +1,49 @@ +using AIStudio.Provider; +using AIStudio.Settings; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Checks whether the Model Comparison assistant may use a provider, for both of its modes. +/// +public static class ModelComparisonProviderCheck +{ + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(ModelComparisonProviderCheck).Namespace, nameof(ModelComparisonProviderCheck)); + + /// + /// Tells why the assistant may not use a provider which was selected, e.g., when it was saved in a preset or a + /// comparison series: an organization may prohibit a provider or raise the confidence requirements at any time. + /// + /// The settings. + /// The provider. + /// The reason, or null when the assistant may use the provider. + public static string? IssueOf(SettingsManager settingsManager, AIStudio.Settings.Provider provider) + { + if (provider.UsedLLMProvider is LLMProviders.NONE) + return TA("Please select a provider."); + + if (settingsManager.IsProviderProhibited(provider.UsedLLMProvider)) + return TA("The use of this provider has been prohibited by your organization. Please select a different provider."); + + if (!settingsManager.IsProviderConfident(provider, Tools.Components.MODEL_COMPARISON_ASSISTANT)) + return TA("This provider does not meet the confidence requirements of this assistant. Please choose another one."); + + return null; + } + + /// + /// Gets the provider of a model which a comparison series keeps, as long as it still uses the same model as when + /// the series began. + /// + /// The settings. + /// The ID of the provider. + /// The model as it was when the series began. + /// The provider, or NONE when it was deleted or uses another model now. + public static AIStudio.Settings.Provider ProviderAsItWas(SettingsManager settingsManager, string providerId, ModelComparisonModelIdentity identity) + { + var provider = settingsManager.GetProviderById(providerId); + return provider.UsedLLMProvider is not LLMProviders.NONE && ModelComparisonModelIdentity.Of(provider).IsSameModelAs(identity) + ? provider + : AIStudio.Settings.Provider.NONE; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonQuestionPreview.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonQuestionPreview.cs new file mode 100644 index 000000000..e25a1030f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonQuestionPreview.cs @@ -0,0 +1,30 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Shortens a question to what a summary or a stored result keeps of it, so that the user recognizes it again. +/// +public static class ModelComparisonQuestionPreview +{ + /// + /// How many characters of a question the Model Comparison assistant keeps at most. + /// + public const int MAX_CHARACTERS = 300; + + /// + /// Shortens a question, and marks the cut. + /// + /// The question. + /// The number of characters of the question to keep, at most. + /// The question, or its beginning followed by an ellipsis. + public static string Of(string question, int maxCharacters = MAX_CHARACTERS) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxCharacters); + + if (question.Length <= maxCharacters) + return question; + + // Cutting between the two halves of a character outside the basic plane, an emoji for example, would leave half of it: + var length = char.IsHighSurrogate(question[maxCharacters - 1]) ? maxCharacters - 1 : maxCharacters; + return question[..length].TrimEnd() + "…"; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor new file mode 100644 index 000000000..c1634b393 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor @@ -0,0 +1,32 @@ +@inherits MSGComponentBase + + + + + + @T("Model") + @T("Wins") + @T("Ties") + @T("Losses") + @T("Share of wins") + @T("Likely range") + @T("Assessment") + + + + @foreach (var standing in this.Standings) + { + + @standing.Model.Label + @standing.Wins.CompactCount() + @standing.Ties.CompactCount() + @standing.Losses.CompactCount() + @ModelComparisonFormat.Share(standing.WinShare.Share) + @this.FormatRange(standing.WinShare) + + + + + } + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor.cs new file mode 100644 index 000000000..ccf3ba9ef --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRanking.razor.cs @@ -0,0 +1,47 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// A ranking of the models: their wins, ties, and losses as a chart, and as a table with their share of wins and how +/// sure that share is. +/// +public partial class ModelComparisonRanking : MSGComponentBase +{ + /// + /// Gets or sets the models, best first, by the votes of the user or by the verdicts of the judges. + /// + [Parameter, EditorRequired] + public IReadOnlyList Standings { get; set; } = []; + + /// + /// The wins, ties, and losses of each model, as the three series of a stacked bar chart. + /// + private List> OutcomeSeries => + [ + new() { Name = T("Wins"), Data = new([.. this.Standings.Select(standing => (double)standing.Wins)]) }, + new() { Name = T("Ties"), Data = new([.. this.Standings.Select(standing => (double)standing.Ties)]) }, + new() { Name = T("Losses"), Data = new([.. this.Standings.Select(standing => (double)standing.Losses)]) }, + ]; + + private StackedBarChartOptions ChartOptions => new() + { + ChartPalette = ModelComparisonCharts.OUTCOME_PALETTE, + YAxisTicks = ModelComparisonCharts.AxisStep(this.Standings.Select(standing => (double)(standing.Wins + standing.Ties + standing.Losses))), + YAxisToStringFunc = value => ((long)value).CompactCount(), + XAxisLabelRotation = ModelComparisonCharts.LabelRotation(this.Standings.Count), + }; + + private string FormatRange(ModelComparisonWinShare winShare) => winShare is { LowerBound: { } lower, UpperBound: { } upper } + ? string.Format(ModelComparisonFormat.Culture, T("{0} to {1}"), ModelComparisonFormat.Share(lower), ModelComparisonFormat.Share(upper)) + : "-"; + + private string SignificanceText(ModelComparisonSignificance significance) => significance switch + { + ModelComparisonSignificance.CLEARLY_BETTER => T("Clearly better"), + ModelComparisonSignificance.CLEARLY_WORSE => T("Clearly worse"), + _ => T("Not yet clear"), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecord.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecord.cs new file mode 100644 index 000000000..f451d31ee --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecord.cs @@ -0,0 +1,88 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the Model Comparison assistant keeps of a batch, so that the statistics can tell later which model came out +/// ahead across many comparisons. +/// +/// +/// Only the results are kept: which models took part, the votes, the choices of the judge, and the times. Neither the +/// answers, nor the context, nor the reasoning or the instructions of the judge are kept; the question only in short, +/// so that the user recognizes a comparison again. A stored comparison names its file by the ID of its batch, so +/// storing the same batch again, after the session was restored for example, replaces the file instead of counting +/// the batch twice. +/// +public sealed record ModelComparisonRecord +{ + /// + /// The ID of the batch, which names the file. + /// + public Guid Id { get; set; } + + /// + /// When the user was through with the batch, in UTC. + /// + public DateTimeOffset FinishedAtUtc { get; set; } + + /// + /// The beginning of the question, as the summary of the batch showed it. + /// + public string QuestionPreview { get; set; } = string.Empty; + + /// + /// The first model. + /// + public ModelComparisonModelIdentity First { get; set; } = new(); + + /// + /// The second model. + /// + public ModelComparisonModelIdentity Second { get; set; } = new(); + + /// + /// The judge, or null when the batch had none. + /// + public ModelComparisonModelIdentity? Judge { get; set; } + + /// + /// The runs which arrived, in the order they arrived. + /// + public List Runs { get; set; } = []; + + /// + /// Whether any run holds a vote or a choice of the judge; only then the batch is worth keeping. + /// + [JsonIgnore] + public bool HoldsAnyChoice => this.Runs.Any(run => run.Vote is not null || run.JudgeChoice is not null); + + /// + /// Gets what the assistant keeps of a batch. + /// + /// The batch, which the user is through with. + /// When the user was through with it. + /// The beginning of the question. + /// The first model. + /// The second model. + /// The judge, or null without one. + /// The stored comparison. + public static ModelComparisonRecord Of(ModelComparisonBatch batch, DateTimeOffset finishedAtUtc, string questionPreview, ModelComparisonModelIdentity first, ModelComparisonModelIdentity second, ModelComparisonModelIdentity? judge) => new() + { + Id = batch.Id, + FinishedAtUtc = finishedAtUtc.ToUniversalTime(), + QuestionPreview = questionPreview, + First = first, + Second = second, + Judge = judge, + Runs = + [ + .. batch.Entries.Select((entry, index) => new ModelComparisonRecordRun + { + Vote = batch.VoteChoiceAt(index), + JudgeChoice = batch.JudgeChoiceAt(index), + First = ModelComparisonRecordAnswer.Of(entry.Result.First), + Second = ModelComparisonRecordAnswer.Of(entry.Result.Second), + }), + ], + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordAnswer.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordAnswer.cs new file mode 100644 index 000000000..ae8ff9f11 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordAnswer.cs @@ -0,0 +1,35 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How a model did in one run of a stored comparison: whether it answered, and how long it took. The answer itself is +/// not stored. +/// +public sealed record ModelComparisonRecordAnswer +{ + /// + /// Whether the model answered. + /// + public bool Answered { get; set; } + + /// + /// The time until the first piece of the answer arrived, or null when nothing arrived. + /// + public TimeSpan? FirstTokenTime { get; set; } + + /// + /// The time until the answer was complete, or null when it never was. + /// + public TimeSpan? TotalTime { get; set; } + + /// + /// Gets what a stored comparison keeps of an answer. + /// + /// The answer. + /// Whether it was complete, and its times. + public static ModelComparisonRecordAnswer Of(ModelComparisonAnswer answer) => new() + { + Answered = answer.Completed, + FirstTokenTime = answer.FirstTokenTime, + TotalTime = answer.TotalTime, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordRun.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordRun.cs new file mode 100644 index 000000000..a2f423eff --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRecordRun.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// One run of a stored comparison, in the terms of the models rather than the columns: the columns were drawn fresh +/// for every run and mean nothing once the run is over. +/// +public sealed record ModelComparisonRecordRun +{ + /// + /// The model the user preferred, a tie, or null when the user did not vote on the run. + /// + public ModelComparisonModelChoice? Vote { get; set; } + + /// + /// The model the judge preferred, a tie, or null without a usable verdict of a judge. + /// + public ModelComparisonModelChoice? JudgeChoice { get; set; } + + /// + /// How the first model did. + /// + public ModelComparisonRecordAnswer First { get; set; } = new(); + + /// + /// How the second model did. + /// + public ModelComparisonRecordAnswer Second { get; set; } = new(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRequest.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRequest.cs new file mode 100644 index 000000000..628d2cb55 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRequest.cs @@ -0,0 +1,77 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What both models are asked: an optional context, such as a document, and the question about it. The page calls the +/// context the material, and so does the prompt. +/// +/// +/// Both models get exactly the text ToPrompt returns, in the same shape, and nothing +/// else. That is what makes the answers comparable: a difference in the wording of a prompt would +/// count as a difference between the models. +/// +public sealed record ModelComparisonRequest +{ + /// + /// Creates the request from what the user entered. Leading and trailing white space is dropped, + /// so a stray line break at the end of a pasted document does not count as text. + /// + /// The context, or an empty text when there is none. + /// The question or task. + /// + /// The language both models are asked to answer in, by its English name (for example + /// "German"), or an empty text to leave the language to the models themselves. + /// + public ModelComparisonRequest(string context, string question, string languageName = "") + { + this.Context = context.Trim(); + this.Question = question.Trim(); + this.LanguageName = languageName.Trim(); + } + + public string Context { get; } + + public string Question { get; } + + public string LanguageName { get; } + + /// + /// Shortens the question to something which fits into a summary, and marks the cut, see + /// ModelComparisonQuestionPreview. + /// + /// The number of characters of the question to keep, at most. + /// The question, or its beginning followed by an ellipsis. + public string GetQuestionPreview(int maxCharacters = ModelComparisonQuestionPreview.MAX_CHARACTERS) => ModelComparisonQuestionPreview.Of(this.Question, maxCharacters); + + /// + /// Builds the prompt. Without a context, it is the question alone: headings around a lone + /// question would only give the models something to comment on. + /// + /// + /// An identifier to append as a plainly labeled, ignorable line, or an empty text to leave the + /// prompt exactly as asked. Comparing a model against itself relies on asking it more than once, + /// and a byte-identical prompt is exactly what a caching gateway between here and the model -- + /// LiteLLM among them -- is built to recognize and answer from its cache instead of asking again. + /// A fixed request would silently stop being independent samples without this. + /// + /// + /// The level of the headings, 1 for the prompt of the models. A prompt which quotes the request below a heading of + /// its own, as the one of the judge does, asks for 2, so that the request stays one part of it. + /// + /// The user prompt both models receive. + public string ToPrompt(string cacheBuster = "", int headingLevel = 1) + { + ArgumentOutOfRangeException.ThrowIfLessThan(headingLevel, 1); + var heading = new string('#', headingLevel); + var body = this.Context.Length == 0 + ? this.Question + : $"{heading} Material\n{this.Context}\n\n{heading} Question or task\n{this.Question}"; + + var withLanguage = this.LanguageName.Length == 0 + ? body + : $"{body}\n\nAnswer in {this.LanguageName}."; + + return cacheBuster.Length == 0 + ? withLanguage + : $"{withLanguage}\n\n(Request ID: {cacheBuster}. This has no bearing on your answer; it is only here so repeated requests are not answered from a cache.)"; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultSource.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultSource.cs new file mode 100644 index 000000000..d3a1f05e1 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultSource.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Which results of the Model Comparison assistant the statistics count. +/// +public enum ModelComparisonResultSource +{ + /// + /// The compared answers of the stability mode, and the blind tasks of every comparison series whose result the user + /// saw. + /// + ALL, + + /// + /// Only the compared answers of the stability mode. + /// + ANSWERS, + + /// + /// Only the blind tasks of every comparison series whose result the user saw. + /// + TASKS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultTexts.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultTexts.cs new file mode 100644 index 000000000..4d5793e39 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonResultTexts.cs @@ -0,0 +1,45 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The long explanations of the results of the Model Comparison assistant, which its dialog of all results, the result +/// of a comparison series, and the summary of a request share. +/// +public static class ModelComparisonResultTexts +{ + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(ModelComparisonResultTexts).Namespace, nameof(ModelComparisonResultTexts)); + + /// + /// What the dialog of all results shows, and what its filter does. + /// + public static string WhatTheResultsShow => TA("Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them."); + + /// + /// How to read the share of wins, its likely range, and the assessment. + /// + public static string HowToReadTheShares => TA("The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too."); + + /// + /// What the assessment of a pair of models means. + /// + public static string WhatThePairAssessmentMeans => TA("The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share."); + + /// + /// What the columns of the agreement of the judges mean, and what a high agreement tells. + /// + public static string WhatTheAgreementMeans => TA("“Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself."); + + /// + /// What the result of a comparison series shows. + /// + public static string WhatTheSeriesResultShows => TA("The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you."); + + /// + /// What the effort of a model in a comparison series means. + /// + public static string WhatTheEffortMeans => TA("The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner."); + + /// + /// What the summary of the runs of one request shows, and what it cannot tell. + /// + public static string WhatTheBatchSummaryShows => TA("Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together."); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor new file mode 100644 index 000000000..96083dcdf --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor @@ -0,0 +1,38 @@ +@inherits MSGComponentBase + +@* A single run is open from the start: there is nothing else to look at. *@ + + +
+ + @this.Heading + + + @if (this.JudgeChoice is { } choice) + { + + @this.NameOf(choice) + + } +
+
+ + @* From the breakpoint md on, both cards in one row and both captions in the next; in a narrower window, each + caption right below its answer, with a line between the two answers. *@ + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor.cs new file mode 100644 index 000000000..9ef2215db --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunPanel.razor.cs @@ -0,0 +1,63 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// One run in the summary of a batch, with the names of the models: closed, a line with the vote of the user and the +/// choice of the judge; open, both answers, the outcome, the times, and the verdict of the judge with its reasoning. +/// +/// +/// The answers stand in the order of the models, not in the columns of the vote: the columns were drawn fresh for every +/// run, and across several runs, only the names say anything. +/// +public partial class ModelComparisonRunPanel : MSGComponentBase +{ + /// + /// Gets or sets the batch. + /// + [Parameter, EditorRequired] + public ModelComparisonBatch Batch { get; set; } = null!; + + /// + /// Gets or sets the position of the run, zero-based. + /// + [Parameter, EditorRequired] + public int Index { get; set; } + + private ModelComparisonBatchEntry Entry => this.Batch.Entries[this.Index]; + + private ModelComparisonModelChoice? VoteChoice => this.Batch.VoteChoiceAt(this.Index); + + private ModelComparisonModelChoice? JudgeChoice => this.Batch.JudgeChoiceAt(this.Index); + + /// + /// Names a model of the run, or says that both answers were equally good. + /// + /// A model, or a tie. + private string NameOf(ModelComparisonModelChoice choice) => choice is ModelComparisonModelChoice.TIE + ? T("Equally good") + : this.Entry.Result.AnswerOf(choice).Label; + + /// + /// Says in the title of the run what the user chose. + /// + private string Heading + { + get + { + var runNumber = this.Index + 1; + if (!this.Entry.Result.BothCompleted) + return string.Format(ModelComparisonFormat.Culture, T("Run {0}: at least one model did not answer"), runNumber); + + return this.VoteChoice switch + { + null => string.Format(ModelComparisonFormat.Culture, TA("Run {0}: without your vote"), runNumber), + ModelComparisonModelChoice.TIE => string.Format(ModelComparisonFormat.Culture, TA("Run {0}: you found both answers equally good"), runNumber), + + { } chosen => string.Format(ModelComparisonFormat.Culture, TA("Run {0}: you preferred {1}"), runNumber, this.NameOf(chosen)), + }; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor new file mode 100644 index 000000000..49a9662aa --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor @@ -0,0 +1,47 @@ +@inherits MSGComponentBase + + + @T("Progress of the runs") + + + @for (var i = 0; i < this.Batch.RunCount; i++) + { + var index = i; + var status = this.Batch.StatusOf(index); + @* A native title attribute, not MudTooltip: while the batch is still running, this row re-renders often enough that MudTooltip's JS-side hover tracking kept losing track of which badge is which, and stopped showing anything at all. A native tooltip lives entirely in the browser, so re-rendering the content underneath it cannot break it. *@ + @* The current entry gets a ring around its avatar: with several runs done and unvoted, position alone -- second from the left, say -- is too easy to lose track of. *@ + + + + @switch (status) + { + case ModelComparisonRunStatus.VOTED: + + break; + + case ModelComparisonRunStatus.RUNNING: + @* Only the runs actually in flight get the spinner -- the ones still queued behind the concurrency limit get an hourglass instead: *@ + + break; + + case ModelComparisonRunStatus.QUEUED: + + break; + + case ModelComparisonRunStatus.NO_USABLE_ANSWER or ModelComparisonRunStatus.VOTE_SKIPPED: + + break; + + default: + + break; + } + + + + @(index + 1) + + + } + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor.cs new file mode 100644 index 000000000..44cfb12de --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunProgress.razor.cs @@ -0,0 +1,47 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The progress of a batch with several runs: one badge per run, which says whether it still runs, waits for the vote, +/// or is done. +/// +public partial class ModelComparisonRunProgress : MSGComponentBase +{ + /// + /// Gets or sets the batch. + /// + [Parameter, EditorRequired] + public ModelComparisonBatch Batch { get; set; } = null!; + + /// + /// Gets or sets whether a judge takes part in the batch. + /// + [Parameter] + public bool JudgeEnabled { get; set; } + + /// + /// Says what the badge of a run means, for the tooltip over it: the badge alone only shows a color and a small icon, + /// not enough to be sure what it stands for. + /// + /// The position of the run, zero-based. + private string DescribeStatus(int index) + { + var text = this.Batch.StatusOf(index) switch + { + ModelComparisonRunStatus.QUEUED => T("Run {0}: queued, waiting for a free slot."), + ModelComparisonRunStatus.RUNNING => T("Run {0}: still working."), + ModelComparisonRunStatus.NO_USABLE_ANSWER => T("Run {0}: no usable answer, nothing to vote on."), + ModelComparisonRunStatus.VOTED => T("Run {0}: voted on."), + ModelComparisonRunStatus.VOTE_SKIPPED => TA("Run {0}: your vote was skipped."), + + _ when this.JudgeEnabled && !this.Batch.HasJudgeVerdict(index) => TA("Run {0}: the judge gave no usable verdict, your vote is missing."), + _ when this.JudgeEnabled => TA("Run {0}: the models and the judge are done, your vote is missing."), + _ => TA("Run {0}: the models are done, your vote is missing."), + }; + + return string.Format(ModelComparisonFormat.Culture, text, index + 1); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunResult.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunResult.cs new file mode 100644 index 000000000..118f8853d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunResult.cs @@ -0,0 +1,42 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What a comparison run brought back: the answers of the two models. +/// +public sealed record ModelComparisonRunResult +{ + /// + /// When the run ended. Both answers are shown with this one time: a time of their own would + /// tell the faster model from the slower one. + /// + public DateTimeOffset Time { get; init; } = DateTimeOffset.Now; + + public required ModelComparisonAnswer First { get; init; } + + public required ModelComparisonAnswer Second { get; init; } + + /// + /// What an optional judge said about the two answers, or null when no judge was asked, or when + /// there was nothing for it to judge because at least one model did not answer. + /// + public ModelComparisonJudgeVerdict? Judge { get; init; } + + /// + /// Whether both models answered. Only then there is anything to vote on: a vote between an + /// answer and a failure would say nothing about the models. + /// + public bool BothCompleted => this.First.Completed && this.Second.Completed; + + /// + /// Gets the answer of a model of the run. + /// + /// The first or the second model; a tie names no model. + /// The answer of that model. + public ModelComparisonAnswer AnswerOf(ModelComparisonModelChoice model) => model switch + { + ModelComparisonModelChoice.FIRST => this.First, + ModelComparisonModelChoice.SECOND => this.Second, + + _ => throw new ArgumentOutOfRangeException(nameof(model), model, $"The choice '{model}' names no model."), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunStatus.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunStatus.cs new file mode 100644 index 000000000..ac8aca959 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunStatus.cs @@ -0,0 +1,37 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Where a run of a batch stands, as its badge in the progress of the batch shows it. +/// +public enum ModelComparisonRunStatus +{ + /// + /// Started, but waiting for a free slot among the runs which ask the models at the same time. + /// + QUEUED, + + /// + /// Asking the models right now. + /// + RUNNING, + + /// + /// Arrived, but at least one model gave no usable answer, so there is nothing to vote on. + /// + NO_USABLE_ANSWER, + + /// + /// Arrived with both answers; the vote of the user is still to come. + /// + AWAITING_VOTE, + + /// + /// The user voted on it. + /// + VOTED, + + /// + /// Arrived with both answers, but the user moved past it without voting. + /// + VOTE_SKIPPED, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunner.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunner.cs new file mode 100644 index 000000000..9b3dbcbc2 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonRunner.cs @@ -0,0 +1,211 @@ +using System.Text; + +using AIStudio.Chat; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Sends one request to two models at the same time and measures how each of them does. An optional +/// third model, the judge, is asked about the two answers once both are in. +/// +/// +/// The two requests do not depend on each other. A model which fails, or takes very long, costs +/// the other one nothing, and only a cancellation of the token ends both. Nothing here throws for a +/// model which did not answer: that is a result to report, and the caller has to be able to show it +/// next to the answer of the other model. The judge is different: it needs both answers, so it runs +/// only after them, not alongside them. +/// +/// Every await here uses ConfigureAwait(false): this class is invoked from a Blazor Server +/// page (ModelComparisonBatchRunner, in turn from the assistant itself), and the first of +/// them would otherwise still be running on that page's own synchronisation context -- caught before +/// its own first suspension, the same way ModelComparisonBatchRunner.RunOneAsync is. Without +/// this, every chunk streamed from a model, the judge included, would try to resume on that same +/// context, the very thing a click on the page also has to wait its turn on. +/// +/// Where to say why a model did not answer. The users see no reason: the text of an error may name a provider. +/// +/// Filters the two answers, or the two chats, before the judge reads them, and gives them back in the same order. In +/// the app, this is the prompt injection filter, see ModelComparisonProviderAdapter.CreateJudgeInputFilter. +/// +/// The clock which measures the times. The system clock, unless a test brings its own. +public sealed class ModelComparisonRunner(ILogger logger, Func, Task>> filterJudgeInputs, TimeProvider? timeProvider = null) +{ + private readonly TimeProvider time = timeProvider ?? TimeProvider.System; + + /// The first model. + /// The second model. + /// What both models are asked. + /// Cancels the run, first model, second model, and judge alike. + /// + /// Which of the two models will stand in column A once the answers are shown. The judge is told + /// the answers as "Answer A" and "Answer B", the same terms the user sees, so this has to be + /// settled before it is asked rather than after. Ignored without a . + /// + /// The optional judge, asked once both models have answered. + /// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations. Ignored without a . + public async Task RunAsync(ModelComparisonParticipant first, ModelComparisonParticipant second, ModelComparisonRequest request, CancellationToken token, ModelComparisonPresentationOrder presentationOrder = ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, ModelComparisonParticipant? judge = null, string judgeInstructions = "") + { + // + // A fresh ID for every run, the same one for both models: it stops a caching gateway between + // here and the models from answering a repeat run out of its cache instead of asking again, + // without making the two models' prompts differ from each other. + // + var prompt = request.ToPrompt(Guid.NewGuid().ToString("N")); + + // + // Task.Run on purpose: a provider may do work before its first await, and without it the + // second model would only be asked once the first one had got that far. + // + var firstTask = Task.Run(() => this.AskAsync(first, prompt, token), CancellationToken.None); + var secondTask = Task.Run(() => this.AskAsync(second, prompt, token), CancellationToken.None); + await Task.WhenAll(firstTask, secondTask).ConfigureAwait(false); + + var firstAnswer = await firstTask.ConfigureAwait(false); + var secondAnswer = await secondTask.ConfigureAwait(false); + + return new ModelComparisonRunResult + { + First = firstAnswer, + Second = secondAnswer, + Judge = await this.AskJudgeAsync(judge, judgeInstructions, request, presentationOrder, firstAnswer, secondAnswer, token).ConfigureAwait(false), + }; + } + + /// + /// Asks the judge once both models have answered. + /// + /// + /// The judge needs both complete answers to judge, so it is asked after them, never alongside + /// them. Nothing here throws: a judge which fails is a verdict which is not completed, the same + /// as a model which did not answer in AskAsync. + /// + private async Task AskJudgeAsync(ModelComparisonParticipant? judge, string judgeInstructions, ModelComparisonRequest request, ModelComparisonPresentationOrder presentationOrder, ModelComparisonAnswer first, ModelComparisonAnswer second, CancellationToken token) + { + if (judge is null || !first.Completed || !second.Completed || token.IsCancellationRequested) + return null; + + var (answerA, answerB) = presentationOrder is ModelComparisonPresentationOrder.FIRST_MODEL_FIRST + ? (first.Text, second.Text) + : (second.Text, first.Text); + + // + // Both answers were written by a model, and a model may repeat what it read, a document of the context for + // example, together with any instructions hidden in it. They reach the judge as part of its prompt, so they + // pass the prompt injection filter first, the same as an answer which is sent on to another assistant. A + // judge which cannot get filtered answers is not asked at all: + // + if (await this.FilterForJudgeAsync(judge, [answerA, answerB]).ConfigureAwait(false) is not { } filteredAnswers) + return ModelComparisonJudgeVerdict.Parse(judge.Label, false, string.Empty); + + var judgeRequest = new ModelComparisonJudgeRequest(request, judgeInstructions, filteredAnswers[0], filteredAnswers[1], request.LanguageName); + var judgeAnswer = await this.AskAsync(judge, judgeRequest.ToPrompt(Guid.NewGuid().ToString("N")), token).ConfigureAwait(false); + + return ModelComparisonJudgeVerdict.Parse(judgeAnswer.Label, judgeAnswer.Completed, judgeAnswer.Text); + } + + /// + /// Asks the judge about the two chats of a task of a comparison series, see ModelComparisonChatJudgeRequest. + /// + /// + /// The chats hold answers of models, so they pass the same filter as the answers of a run before the judge reads + /// them. Nothing here throws: a judge which fails, or which is canceled, gives a verdict which is not completed. + /// + /// The judge. + /// The first chat, see ChatTranscript. + /// The second chat. + /// The language of the reasoning, by its English name, or an empty text to leave it to the judge. + /// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations. + /// Cancels the judge. + /// The verdict: COLUMN_A names the first chat, COLUMN_B the second one. + public async Task JudgeChatsAsync(ModelComparisonParticipant judge, string transcriptOfChatOne, string transcriptOfChatTwo, string languageName, string judgeInstructions, CancellationToken token) + { + if (await this.FilterForJudgeAsync(judge, [transcriptOfChatOne, transcriptOfChatTwo]).ConfigureAwait(false) is not { } filteredChats) + return ModelComparisonJudgeVerdict.Parse(judge.Label, false, string.Empty, ModelComparisonJudgeFormat.CHATS); + + var judgeRequest = new ModelComparisonChatJudgeRequest(filteredChats[0], filteredChats[1], languageName, judgeInstructions); + var judgeAnswer = await this.AskAsync(judge, judgeRequest.ToPrompt(Guid.NewGuid().ToString("N")), token).ConfigureAwait(false); + + return ModelComparisonJudgeVerdict.Parse(judgeAnswer.Label, judgeAnswer.Completed, judgeAnswer.Text, ModelComparisonJudgeFormat.CHATS); + } + + /// + /// Passes what the judge reads through the filter. + /// + /// The judge, for the log. + /// The two texts the judge compares. + /// The filtered texts in the same order, or null when they could not be filtered: then the judge is not asked at all. + private async Task?> FilterForJudgeAsync(ModelComparisonParticipant judge, IReadOnlyList inputs) + { + try + { + return await filterJudgeInputs(inputs).ConfigureAwait(false); + } + catch (Exception exception) + { + logger.LogWarning(exception, "The texts could not be filtered for the judge '{Label}' of the model comparison.", judge.Label); + return null; + } + } + + /// + /// Asks one model. Never throws: whatever goes wrong leaves the answer not completed. + /// + private async Task AskAsync(ModelComparisonParticipant participant, string prompt, CancellationToken token) + { + var text = new StringBuilder(); + var completed = true; + TimeSpan? firstTokenTime = null; + var start = this.time.GetTimestamp(); + + try + { + await foreach (var chunk in participant.Ask(prompt, token).WithCancellation(token).ConfigureAwait(false)) + { + // A model which is asked to stop may still hand over a last chunk: + if (token.IsCancellationRequested) + break; + + if (chunk.Length == 0) + continue; + + firstTokenTime ??= this.time.GetElapsedTime(start); + text.Append(chunk); + } + + if (token.IsCancellationRequested) + completed = false; + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + completed = false; + } + catch (Exception exception) + { + logger.LogWarning(exception, "The model '{Label}' did not answer in the model comparison.", participant.Label); + completed = false; + } + + var totalTime = this.time.GetElapsedTime(start); + var answer = text.ToString().RemoveThinkTags().Trim(); + + // + // A stream which ends cleanly without saying anything is a failure, too. So is one which + // ends inside the thinking of the model: nothing of what is left is an answer. Counted + // as answers, both would go into a vote as an empty text -- and always lose it. + // + if (completed && answer.Length == 0) + { + logger.LogWarning("The model '{Label}' ended without an answer in the model comparison.", participant.Label); + completed = false; + } + + return new ModelComparisonAnswer + { + Label = participant.Label, + Text = answer, + Completed = completed, + FirstTokenTime = firstTokenTime, + TotalTime = completed ? totalTime : null, + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeries.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeries.cs new file mode 100644 index 000000000..37b261d55 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeries.cs @@ -0,0 +1,121 @@ +using System.Text.Json.Serialization; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// A comparison series: two models compared on real tasks of the user, over days or weeks, with the same profile, +/// tools, and data sources for both. +/// +/// +/// The series keeps the providers by their IDs, to ask them, and the identities of their models as they were when the +/// series began, so that a provider whose model changed meanwhile is noticed rather than compared under a wrong name. +/// Its chats lie in the folder of the series, see ModelComparisonStore, and stay readable there. +/// +public sealed record ModelComparisonSeries +{ + /// + /// The ID of the series, which names its folder. + /// + public Guid Id { get; set; } + + /// + /// The name the user gave the series. + /// + public string Name { get; set; } = string.Empty; + + /// + /// When the user began the series, in UTC. + /// + public DateTimeOffset CreatedAtUtc { get; set; } + + /// + /// The ID of the provider of the first model. + /// + public string FirstProviderId { get; set; } = string.Empty; + + /// + /// The ID of the provider of the second model. + /// + public string SecondProviderId { get; set; } = string.Empty; + + /// + /// The first model, as it was when the series began. + /// + public ModelComparisonModelIdentity First { get; set; } = new(); + + /// + /// The second model, as it was when the series began. + /// + public ModelComparisonModelIdentity Second { get; set; } = new(); + + /// + /// The ID of the provider of the judge, or empty without a judge. + /// + public string JudgeProviderId { get; set; } = string.Empty; + + /// + /// The judge, as it was when the series began, or null without a judge. + /// + public ModelComparisonModelIdentity? Judge { get; set; } + + /// + /// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations; + /// empty when the user confirmed that the judge may decide by its own standards. Fixed with the series like its + /// models, so that the judge decides every task by the same standards. + /// + public string JudgeInstructions { get; set; } = string.Empty; + + /// + /// The ID of the profile both chats use. + /// + public string ProfileId { get; set; } = string.Empty; + + /// + /// The tools both chats may use. + /// + public List ToolIds { get; set; } = []; + + /// + /// The IDs of the data sources both chats may search. + /// + public List DataSourceIds { get; set; } = []; + + /// + /// How both chats search the data sources, or null without data sources. The same for both, so that neither model + /// gets passages which the other one has to look for itself. + /// + public DataSourceRetrievalMode? RetrievalMode { get; set; } + + /// + /// Whether the user saw the result of the series, which names the models. Tasks decided afterward are no longer + /// blind. + /// + public bool IsResolved { get; set; } + + /// + /// When the user saw the result of the series first, in UTC, or null before. + /// + public DateTimeOffset? ResolvedAtUtc { get; set; } + + /// + /// The tasks of the series, in the order they were started. + /// + public List Tasks { get; set; } = []; + + /// + /// When the user last worked on the series, in UTC. + /// + [JsonIgnore] + public DateTimeOffset LastActivityUtc => this.Tasks + .Select(task => task.FinishedAtUtc ?? task.StartedAtUtc) + .Append(this.CreatedAtUtc) + .Max(); + + /// + /// The task the user works on right now, or null when every task is decided. + /// + [JsonIgnore] + public ModelComparisonSeriesTask? OpenTask => this.Tasks.LastOrDefault(task => task.State is not ModelComparisonSeriesTaskState.DONE); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChat.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChat.cs new file mode 100644 index 000000000..efa900cac --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChat.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// One of the two chats of a task of a comparison series, as the series keeps it beside the chat itself. +/// +public sealed record ModelComparisonSeriesChat +{ + /// + /// The ID of the chat, which names its file in the folder of the series. + /// + public Guid ChatId { get; set; } + + /// + /// When the first message of the chat was sent, in UTC, or null until the user is done with the chat. Not when the + /// task began: the user writes the first message only for the first chat, and the time it takes would count + /// against the model of that chat alone. + /// + public DateTimeOffset? StartedAtUtc { get; set; } + + /// + /// When the user was done with the chat, in UTC, or null before. + /// + public DateTimeOffset? FinishedAtUtc { get; set; } + + /// + /// How many messages the user wrote in the chat, the first one included. + /// + public int UserMessageCount { get; set; } + + /// + /// How long the user worked in the chat, or null while it is not done. + /// + [JsonIgnore] + public TimeSpan? Duration => this.StartedAtUtc is { } started && this.FinishedAtUtc is { } finished ? finished - started : null; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChatStore.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChatStore.cs new file mode 100644 index 000000000..329c06813 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesChatStore.cs @@ -0,0 +1,18 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Lets the embedded chats of a comparison series live in the folder of the series. +/// +/// The store of the assistant. +/// The ID of the series. +public sealed class ModelComparisonSeriesChatStore(ModelComparisonStore store, Guid seriesId) : IEmbeddedChatStore +{ + /// + public Task LoadThreadAsync(Guid chatId, CancellationToken token = default) => store.LoadSeriesChatAsync(seriesId, chatId, token); + + /// + public Task SaveThreadAsync(ChatThread thread, CancellationToken token = default) => store.SaveSeriesChatAsync(seriesId, thread, token); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesCopy.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesCopy.cs new file mode 100644 index 000000000..685e65df4 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesCopy.cs @@ -0,0 +1,100 @@ +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The setup which a new comparison series takes over from an existing one. +/// +/// +/// A series keeps its setup as it was when it began. Meanwhile, a provider may have been deleted, use another model, +/// or no longer meet the requirements of the assistant, and a profile, a tool, or a data source may be gone. The new +/// series leaves such parts out, and MissingParts tells the user which. Whether both models may use what is left is +/// decided by the selections and by the dialog when it stores the series, as for every new series. +/// +/// The first model, or NONE when the user has to choose it anew. +/// The second model, or NONE when the user has to choose it anew. +/// Whether the series asks a judge. +/// The judge, or NONE without a judge or when the user has to choose it anew. +/// The profile both chats use. +/// The tools both chats may use. +/// The data sources both chats may search, and how they search them. +/// The parts the new series cannot take over. +public sealed record ModelComparisonSeriesCopy( + AIStudio.Settings.Provider First, + AIStudio.Settings.Provider Second, + bool JudgeEnabled, + AIStudio.Settings.Provider Judge, + Profile Profile, + HashSet ToolIds, + DataSourceOptions DataSourceOptions, + IReadOnlyList MissingParts) +{ + /// + /// Takes over the setup of a series, as far as it is still available. + /// + /// The settings. + /// The series whose setup the new one takes over. + /// The tools of the series which the user could still select, see + /// ToolRegistry.FilterSelectableToolIdsAsync. + /// The setup of the new series. + public static ModelComparisonSeriesCopy Of(SettingsManager settingsManager, ModelComparisonSeries series, IReadOnlySet selectableToolIds) + { + List missingParts = []; + + var first = UsableProvider(settingsManager, series.FirstProviderId, series.First); + if (first == AIStudio.Settings.Provider.NONE) + missingParts.Add(ModelComparisonSeriesPart.FIRST_MODEL); + + var second = UsableProvider(settingsManager, series.SecondProviderId, series.Second); + if (second == AIStudio.Settings.Provider.NONE) + missingParts.Add(ModelComparisonSeriesPart.SECOND_MODEL); + + // The user wanted a judge, so the switch stays on, and the dialog asks for another one: + var judgeEnabled = series.Judge is not null && !string.IsNullOrWhiteSpace(series.JudgeProviderId); + var judge = series.Judge is { } judgeIdentity && judgeEnabled + ? UsableProvider(settingsManager, series.JudgeProviderId, judgeIdentity) + : AIStudio.Settings.Provider.NONE; + + if (judgeEnabled && judge == AIStudio.Settings.Provider.NONE) + missingParts.Add(ModelComparisonSeriesPart.JUDGE); + + var profile = settingsManager.GetProfileById(series.ProfileId); + var hadProfile = !string.IsNullOrWhiteSpace(series.ProfileId) && !string.Equals(series.ProfileId, Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase); + if (hadProfile && profile == Profile.NO_PROFILE) + missingParts.Add(ModelComparisonSeriesPart.PROFILE); + + var toolIds = series.ToolIds.Where(selectableToolIds.Contains).ToHashSet(StringComparer.Ordinal); + if (toolIds.Count < series.ToolIds.Distinct(StringComparer.Ordinal).Count()) + missingParts.Add(ModelComparisonSeriesPart.TOOLS); + + List dataSourceIds = PreviewFeatures.PRE_RAG_2024.IsEnabled(settingsManager) + ? [.. series.DataSourceIds.Where(dataSourceId => settingsManager.ConfigurationData.DataSources.Any(dataSource => dataSource.Id == dataSourceId))] + : []; + + if (dataSourceIds.Count < series.DataSourceIds.Count) + missingParts.Add(ModelComparisonSeriesPart.DATA_SOURCES); + + var dataSourceOptions = new DataSourceOptions + { + DisableDataSources = dataSourceIds.Count == 0, + AutomaticDataSourceSelection = false, + AutomaticValidation = false, + RetrievalMode = series.RetrievalMode ?? DataSourceRetrievalMode.SEMANTIC_SEARCH, + PreselectedDataSourceIds = dataSourceIds, + }; + + return new(first, second, judgeEnabled, judge, profile, toolIds, dataSourceOptions, missingParts); + } + + /// + /// Gets the provider of a model of the series, as long as it still uses the same model and the assistant may use it. + /// + private static AIStudio.Settings.Provider UsableProvider(SettingsManager settingsManager, string providerId, ModelComparisonModelIdentity identity) + { + var provider = ModelComparisonProviderCheck.ProviderAsItWas(settingsManager, providerId, identity); + return ModelComparisonProviderCheck.IssueOf(settingsManager, provider) is null + ? provider + : AIStudio.Settings.Provider.NONE; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDataSources.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDataSources.cs new file mode 100644 index 000000000..12761951d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDataSources.cs @@ -0,0 +1,16 @@ +using AIStudio.Settings.DataModel; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The data sources both chats of a comparison series may search, and how they search them. +/// +/// The IDs of the data sources. +/// How both chats search them, or null without any data source. +public readonly record struct ModelComparisonSeriesDataSources(IReadOnlyList DataSourceIds, DataSourceRetrievalMode? RetrievalMode) +{ + /// + /// No data source at all. + /// + public static readonly ModelComparisonSeriesDataSources NONE = new([], null); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor new file mode 100644 index 000000000..367d66af5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor @@ -0,0 +1,20 @@ +@inherits MSGComponentBase + + + @TA("Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat.") + + + + + + @T("Chat 1") + + + + + + @T("Chat 2") + + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor.cs new file mode 100644 index 000000000..c42a403fc --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesDecision.razor.cs @@ -0,0 +1,52 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The decision on a task of a comparison series: both chats side by side, read-only and without the names of the +/// models, and the buttons of the vote. +/// +/// +/// The chats keep their places, the first chat on the left: the order in which the user met the models was drawn at +/// random already, see ModelComparisonSeriesTask.FirstModelIsChatOne, and a vote names a chat, not a model. The chats +/// stand side by side on wide windows and below each other on narrow ones, so that long answers stay readable, each +/// with its button below it, as the answers in the mode Answer Variance, see ModelComparisonVoteColumns. +/// +public partial class ModelComparisonSeriesDecision : MSGComponentBase +{ + private const Tools.Components COMPONENT = Tools.Components.MODEL_COMPARISON_ASSISTANT; + + /// + /// Gets or sets where the chats of the series live. + /// + [Parameter, EditorRequired] + public IEmbeddedChatStore Store { get; set; } = null!; + + /// + /// Gets or sets the task. + /// + [Parameter, EditorRequired] + public ModelComparisonSeriesTask SeriesTask { get; set; } = null!; + + /// + /// Gets or sets how a chat of the series starts, for a chat which has no stored thread. + /// + [Parameter, EditorRequired] + public Func CreateThread { get; set; } = () => new(); + + /// + /// Gets or sets whether the series is being saved right now, which holds the vote back. + /// + [Parameter] + public bool IsSaving { get; set; } + + /// + /// Gets or sets what happens when the user votes: COLUMN_A names the first chat, COLUMN_B the second one. + /// + [Parameter] + public EventCallback Vote { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesEffort.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesEffort.cs new file mode 100644 index 000000000..709712bd6 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesEffort.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How much the user had to do with a model of a comparison series until the task was done. +/// +public sealed record ModelComparisonSeriesEffort +{ + /// + /// The model. + /// + public required ModelComparisonModelIdentity Model { get; init; } + + /// + /// How many decided tasks count. + /// + public int TaskCount { get; init; } + + /// + /// How many messages the user wrote on average, the first one included, or null without a decided task. + /// + public double? AverageUserMessages { get; init; } + + /// + /// How long the user worked with the model in the middle: half of the tasks took less, half took more. The median, + /// since a chat left open over night would distort an average. Null without a decided task. + /// + public TimeSpan? MedianDuration { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudge.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudge.cs new file mode 100644 index 000000000..a27e06585 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudge.cs @@ -0,0 +1,101 @@ +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; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudgeStatus.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudgeStatus.cs new file mode 100644 index 000000000..9f3a05747 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesJudgeStatus.cs @@ -0,0 +1,33 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What became of the judge for a task of a comparison series. +/// +public enum ModelComparisonSeriesJudgeStatus +{ + /// + /// The judge was not asked: the series has none, the user did not decide yet, or the user left before the judge + /// answered. + /// + NOT_ASKED, + + /// + /// The judge gave a usable verdict. + /// + VERDICT, + + /// + /// The judge was asked, but gave no usable verdict. + /// + NO_VERDICT, + + /// + /// Both chats together are too long for the context window of the judge, so it was not asked. + /// + TOO_LONG, + + /// + /// The judge is no longer available as it was when the series began, so it was not asked. + /// + UNAVAILABLE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesPart.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesPart.cs new file mode 100644 index 000000000..40417bce1 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesPart.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// A part of the setup of a comparison series, e.g., one which a new series cannot take over from an existing one. +/// +public enum ModelComparisonSeriesPart +{ + FIRST_MODEL, + SECOND_MODEL, + JUDGE, + PROFILE, + TOOLS, + DATA_SOURCES, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor new file mode 100644 index 000000000..a39b69d4b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor @@ -0,0 +1,68 @@ +@inherits MSGComponentBase + + + @T("Result of the series") + + + @ModelComparisonResultTexts.WhatTheSeriesResultShows + + +@if (this.Statistics.Pairing.ComparisonCount == 0) +{ + + @TA("You decided no task of this series without knowing the models, so it has no result.") + +} +else +{ + + @ModelComparisonResultTexts.WhatThePairAssessmentMeans + + + + + @this.ChatOrderText + + @if (this.Statistics.JudgeAgreement is { ComparedRuns: > 0 } agreement) + { + + @string.Format(ModelComparisonFormat.Culture, TA("The judge chose the same as you in {0} of {1} tasks ({2})."), agreement.Agreements.CompactCount(), agreement.ComparedRuns.CompactCount(), ModelComparisonFormat.Share(agreement.Share)) + + } +} + + + @TA("Your effort with each model") + + + @ModelComparisonResultTexts.WhatTheEffortMeans + + + + + + @T("Model") + @T("Decided tasks") + @TA("Your messages on average") + @T("Median time") + + + + @foreach (var effort in this.Efforts) + { + + @effort.Model.Label + @effort.TaskCount.CompactCount() + @ModelComparisonFormat.Decimal(effort.AverageUserMessages) + @ModelComparisonFormat.Minutes(effort.MedianDuration) + + } + + + +@if (this.Statistics.NotBlindVotes.TotalRunCount > 0) +{ + + @this.NotBlindText + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor.cs new file mode 100644 index 000000000..2ecc06b79 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesResult.razor.cs @@ -0,0 +1,45 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The result of a comparison series, with the names of the models: who won the blind tasks, whether the order of the +/// chats mattered, how often the judge agreed, and how much effort each model took. +/// +public partial class ModelComparisonSeriesResult : MSGComponentBase +{ + /// + /// Gets or sets the statistics of the series. + /// + [Parameter, EditorRequired] + public ModelComparisonSeriesStatistics Statistics { get; set; } = null!; + + private IReadOnlyList Efforts => [this.Statistics.FirstEffort, this.Statistics.SecondEffort]; + + private string BlindTasksText => string.Format(ModelComparisonFormat.Culture, T("Tasks decided without knowing the models: {0}"), this.Statistics.Pairing.ComparisonCount.CompactCount()); + + private string ChatOrderText => string.Format( + ModelComparisonFormat.Culture, + TA("The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there."), + this.Statistics.WinsOfChatOne.CompactCount(), + this.Statistics.WinsOfChatTwo.CompactCount()); + + private string NotBlindText + { + get + { + var votes = this.Statistics.NotBlindVotes; + return string.Format( + ModelComparisonFormat.Culture, + TA("After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models."), + votes.TotalRunCount.CompactCount(), + this.Statistics.Pairing.First.Label, + votes.FirstModelWins.CompactCount(), + this.Statistics.Pairing.Second.Label, + votes.SecondModelWins.CompactCount(), + votes.Ties.CompactCount()); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor new file mode 100644 index 000000000..2db45a184 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor @@ -0,0 +1,16 @@ +@inherits MSGComponentBase + + + + + @this.profileText + + + + @this.toolsText + + + + @this.dataSourcesText + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor.cs new file mode 100644 index 000000000..11f4f2a7d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesSetupLine.razor.cs @@ -0,0 +1,110 @@ +using AIStudio.Components; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.ToolCallingSystem; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// One line with the setup both models of a comparison series work with: the profile, the tools, the data sources, and +/// how both chats search them. +/// +/// +/// The user needs this when deciding which chat helped more: unneeded sources in an answer may come from classic RAG, +/// which searches with every message, rather than from the model. A part which no longer exists is named as such, so +/// that the line still tells what the series began with. +/// +public partial class ModelComparisonSeriesSetupLine : MSGComponentBase +{ + /// + /// Gets or sets the series. + /// + [Parameter, EditorRequired] + public ModelComparisonSeries Series { get; set; } = null!; + + /// + /// Gets or sets the classes of the line. + /// + [Parameter] + public string Class { get; set; } = string.Empty; + + [Inject] + private ToolRegistry ToolRegistry { get; init; } = null!; + + /// + /// The series which the texts below were built from. + /// + private ModelComparisonSeries? shownSeries; + + private string profileText = string.Empty; + private string toolsText = string.Empty; + private string dataSourcesText = string.Empty; + + #region Overrides of ComponentBase + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + if (ReferenceEquals(this.shownSeries, this.Series)) + return; + + this.shownSeries = this.Series; + this.profileText = this.ProfileText(); + this.toolsText = await this.ToolsTextAsync(); + this.dataSourcesText = this.DataSourcesText(); + } + + #endregion + + private string ProfileText() + { + if (string.IsNullOrWhiteSpace(this.Series.ProfileId) || string.Equals(this.Series.ProfileId, Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase)) + return T("No profile"); + + var profile = this.SettingsManager.GetProfileById(this.Series.ProfileId); + return profile == Profile.NO_PROFILE + ? T("Profile: no longer available") + : string.Format(T("Profile: {0}"), profile.Name); + } + + private async Task ToolsTextAsync() + { + var toolIds = this.Series.ToolIds.Distinct(StringComparer.Ordinal).ToList(); + if (toolIds.Count == 0) + return T("No tools"); + + List names = []; + foreach (var toolId in toolIds) + if (await this.ToolRegistry.GetCatalogItemAsync(toolId) is { } item) + names.Add(item.DisplayName); + + return string.Format(T("Tools: {0}"), this.ListOf(names, toolIds.Count - names.Count)); + } + + private string DataSourcesText() + { + if (this.Series.DataSourceIds.Count == 0) + return T("No data sources"); + + var names = this.Series.DataSourceIds + .Select(dataSourceId => this.SettingsManager.ConfigurationData.DataSources.FirstOrDefault(dataSource => dataSource.Id == dataSourceId)?.Name) + .OfType() + .ToList(); + + var retrieval = this.Series.RetrievalMode is DataSourceRetrievalMode.EVERY_MESSAGE ? T("classic RAG") : T("Semantic Search"); + return string.Format(T("Data sources: {0} ({1})"), this.ListOf(names, this.Series.DataSourceIds.Count - names.Count), retrieval); + } + + /// + /// Lists names, followed by how many no longer exist. + /// + private string ListOf(List names, int missingCount) + { + if (missingCount > 0) + names.Add(string.Format(T("{0} no longer available"), missingCount)); + + return string.Join(", ", names); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesStatistics.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesStatistics.cs new file mode 100644 index 000000000..3ebfb0984 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesStatistics.cs @@ -0,0 +1,97 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What a comparison series says about its two models, with their names, once the user asked for the result. +/// +/// +/// The wins count only the blind tasks: a task decided after the result was shown is not blind, and is shown apart. +/// Whether the chat a model answered in mattered shows in the wins of the first and of the second chat: the user knows +/// the task better in the second chat, and when that chat wins much more often, the order influenced the decisions. +/// The effort counts every decided task, since the user does not choose it. +/// +public sealed record ModelComparisonSeriesStatistics +{ + /// + /// Both models of the series against each other, by the blind votes and the verdicts of the judge on those tasks. + /// ComparisonCount counts the blind tasks. + /// + public required ModelComparisonPairing Pairing { get; init; } + + /// + /// The votes on the tasks decided after the result was shown. + /// + public ModelComparisonTally NotBlindVotes { get; init; } = ModelComparisonTally.NONE; + + /// + /// How many blind tasks the model in the first chat won. + /// + public int WinsOfChatOne { get; init; } + + /// + /// How many blind tasks the model in the second chat won. + /// + public int WinsOfChatTwo { get; init; } + + /// + /// How often the judge chose the same as the user on the blind tasks, or null without a judge. + /// + public ModelComparisonJudgeAgreement? JudgeAgreement { get; init; } + + /// + /// The effort with the first model. + /// + public required ModelComparisonSeriesEffort FirstEffort { get; init; } + + /// + /// The effort with the second model. + /// + public required ModelComparisonSeriesEffort SecondEffort { get; init; } + + /// + /// Computes the statistics of a series. + /// + /// The series. + /// The statistics. + public static ModelComparisonSeriesStatistics Of(ModelComparisonSeries series) + { + var decided = series.Tasks.Where(task => task.State is ModelComparisonSeriesTaskState.DONE).ToList(); + var blind = decided.Where(task => task.IsBlind).ToList(); + var votesWithWinner = blind.Where(task => task.Vote is ModelComparisonModelChoice.FIRST or ModelComparisonModelChoice.SECOND).ToList(); + var judged = blind.Where(task => task is { Vote: not null, JudgeChoice: not null }).ToList(); + + return new() + { + Pairing = new() + { + First = series.First, + Second = series.Second, + ComparisonCount = blind.Count, + Votes = ModelComparisonTally.Of(blind.Select(task => task.Vote)), + JudgeVerdicts = ModelComparisonTally.Of(blind.Select(task => task.JudgeChoice)), + }, + NotBlindVotes = ModelComparisonTally.Of(decided.Where(task => !task.IsBlind).Select(task => task.Vote)), + WinsOfChatOne = votesWithWinner.Count(task => task.ChatNumberOf(task.Vote!.Value) == 1), + WinsOfChatTwo = votesWithWinner.Count(task => task.ChatNumberOf(task.Vote!.Value) == 2), + JudgeAgreement = series.Judge is { } judge + ? new() { Judge = judge, ComparedRuns = judged.Count, Agreements = judged.Count(task => task.Vote == task.JudgeChoice) } + : null, + FirstEffort = EffortOf(series.First, decided.Select(task => task.ChatOfFirstModel)), + SecondEffort = EffortOf(series.Second, decided.Select(task => task.ChatOfSecondModel)), + }; + } + + private static ModelComparisonSeriesEffort EffortOf(ModelComparisonModelIdentity model, IEnumerable chats) + { + var finishedChats = chats.Where(chat => chat.Duration is not null).ToList(); + if (finishedChats.Count == 0) + return new() { Model = model }; + + return new() + { + Model = model, + TaskCount = finishedChats.Count, + AverageUserMessages = finishedChats.Average(chat => chat.UserMessageCount), + MedianDuration = ModelComparisonStatisticsBuilder.Median([.. finishedChats.Select(chat => chat.Duration!.Value)]), + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTask.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTask.cs new file mode 100644 index 000000000..38661f614 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTask.cs @@ -0,0 +1,223 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// A task of a comparison series: the user works on it with one model in the first chat, then with the other model in +/// the second chat, and decides which chat helped more. +/// +/// +/// The vote and the choice of the judge are kept in terms of the models of the series, FIRST for its first model, not +/// in terms of the chats: which model the first chat had is chosen at random for every task, so that the order in which +/// the user meets them does not favor one model. +/// +public sealed record ModelComparisonSeriesTask +{ + /// + /// The ID of the task. + /// + public Guid Id { get; set; } + + /// + /// When the user started the task, in UTC. + /// + public DateTimeOffset StartedAtUtc { get; set; } + + /// + /// When the user decided, in UTC, or null before. + /// + public DateTimeOffset? FinishedAtUtc { get; set; } + + /// + /// The beginning of the first message, so that the user recognizes the task again. + /// + public string QuestionPreview { get; set; } = string.Empty; + + /// + /// Whether the first model of the series answers in the first chat; otherwise, the second model does. + /// + public bool FirstModelIsChatOne { get; set; } + + /// + /// The chat the user works in first. + /// + public ModelComparisonSeriesChat ChatOne { get; set; } = new(); + + /// + /// The chat the user works in second, which starts with the same first message. + /// + public ModelComparisonSeriesChat ChatTwo { get; set; } = new(); + + /// + /// How far the task got. + /// + public ModelComparisonSeriesTaskState State { get; set; } = ModelComparisonSeriesTaskState.FIRST_CHAT; + + /// + /// The model the user chose, in terms of the models of the series, or null before the decision. + /// + public ModelComparisonModelChoice? Vote { get; set; } + + /// + /// The model the judge chose, in terms of the models of the series, or null without a usable verdict. + /// + public ModelComparisonModelChoice? JudgeChoice { get; set; } + + /// + /// Why the judge chose so, or empty without a usable verdict. It names the chats as "Chat 1" and "Chat 2", never a + /// model. + /// + public string JudgeReasoning { get; set; } = string.Empty; + + /// + /// What became of the judge for this task. + /// + public ModelComparisonSeriesJudgeStatus JudgeStatus { get; set; } = ModelComparisonSeriesJudgeStatus.NOT_ASKED; + + /// + /// Whether the user decided without knowing the models, i.e., before the result of the series was shown. + /// + public bool IsBlind { get; set; } = true; + + /// + /// The chat in which the first model of the series answers. + /// + [JsonIgnore] + public ModelComparisonSeriesChat ChatOfFirstModel => this.FirstModelIsChatOne ? this.ChatOne : this.ChatTwo; + + /// + /// The chat in which the second model of the series answers. + /// + [JsonIgnore] + public ModelComparisonSeriesChat ChatOfSecondModel => this.FirstModelIsChatOne ? this.ChatTwo : this.ChatOne; + + /// + /// Which model of the series answers in the first chat, which the decision shows on the left. + /// + [JsonIgnore] + public ModelComparisonPresentationOrder Order => this.FirstModelIsChatOne + ? ModelComparisonPresentationOrder.FIRST_MODEL_FIRST + : ModelComparisonPresentationOrder.SECOND_MODEL_FIRST; + + /// + /// The chat the user works in right now, or null while the user decides or after the decision. + /// + [JsonIgnore] + public ModelComparisonSeriesChat? CurrentChat => this.State switch + { + ModelComparisonSeriesTaskState.FIRST_CHAT => this.ChatOne, + ModelComparisonSeriesTaskState.SECOND_CHAT => this.ChatTwo, + + _ => null, + }; + + /// + /// Begins a task: the user works in the first chat now. + /// + /// When the user began the task, in UTC. + /// Whether the first model of the series answers in the first chat, drawn at random by the caller. + /// The task. + public static ModelComparisonSeriesTask Begin(DateTimeOffset startedAtUtc, bool firstModelIsChatOne) => new() + { + Id = Guid.NewGuid(), + StartedAtUtc = startedAtUtc, + FirstModelIsChatOne = firstModelIsChatOne, + ChatOne = new() { ChatId = Guid.NewGuid() }, + ChatTwo = new() { ChatId = Guid.NewGuid() }, + State = ModelComparisonSeriesTaskState.FIRST_CHAT, + }; + + /// + /// Ends the work in the first chat: the user works in the second chat now, which starts with the same first message. + /// + /// When the user sent the first message of the first chat, in UTC. + /// When the user was done with the first chat, in UTC. + /// How many messages the user wrote in the first chat. + /// The beginning of the first message, see ModelComparisonQuestionPreview. + /// True when the task was in the first chat; otherwise, nothing changes. + public bool FinishFirstChat(DateTimeOffset startedAtUtc, DateTimeOffset finishedAtUtc, int userMessageCount, string questionPreview) + { + if (this.State is not ModelComparisonSeriesTaskState.FIRST_CHAT) + return false; + + this.ChatOne.StartedAtUtc = startedAtUtc; + this.ChatOne.FinishedAtUtc = finishedAtUtc; + this.ChatOne.UserMessageCount = userMessageCount; + this.QuestionPreview = questionPreview; + this.State = ModelComparisonSeriesTaskState.SECOND_CHAT; + return true; + } + + /// + /// Ends the work in the second chat: the user decides now which chat helped more. + /// + /// When the first message of the second chat was sent, in UTC. + /// When the user was done with the second chat, in UTC. + /// How many messages the user wrote in the second chat, the first one included. + /// True when the task was in the second chat; otherwise, nothing changes. + public bool FinishSecondChat(DateTimeOffset startedAtUtc, DateTimeOffset finishedAtUtc, int userMessageCount) + { + if (this.State is not ModelComparisonSeriesTaskState.SECOND_CHAT) + return false; + + this.ChatTwo.StartedAtUtc = startedAtUtc; + this.ChatTwo.FinishedAtUtc = finishedAtUtc; + this.ChatTwo.UserMessageCount = userMessageCount; + this.State = ModelComparisonSeriesTaskState.DECIDING; + return true; + } + + /// + /// Takes the decision of the user, which names a chat, and keeps it in terms of the models of the series. + /// + /// The chat the user chose: COLUMN_A for the first chat, COLUMN_B for the second one. + /// When the user decided, in UTC. + /// Whether the user decided without knowing the models. + /// True when the user was deciding; otherwise, nothing changes. + public bool Decide(ModelComparisonVote vote, DateTimeOffset decidedAtUtc, bool isBlind) + { + if (this.State is not ModelComparisonSeriesTaskState.DECIDING) + return false; + + this.Vote = this.Order.ToModelChoice(vote); + this.FinishedAtUtc = decidedAtUtc; + this.IsBlind = isBlind; + this.State = ModelComparisonSeriesTaskState.DONE; + return true; + } + + /// + /// Takes the verdict of the judge about both chats, and keeps it in terms of the models of the series. + /// + /// The verdict, in which COLUMN_A names the first chat, see ModelComparisonRunner.JudgeChatsAsync. + public void TakeVerdict(ModelComparisonJudgeVerdict verdict) + { + if (verdict is { Completed: true, Preferred: { } preferred }) + { + this.JudgeChoice = this.Order.ToModelChoice(preferred); + this.JudgeReasoning = verdict.Reasoning; + this.JudgeStatus = ModelComparisonSeriesJudgeStatus.VERDICT; + return; + } + + this.SkipJudge(ModelComparisonSeriesJudgeStatus.NO_VERDICT); + } + + /// + /// Notes why the judge has no verdict on this task. + /// + /// Why; anything but VERDICT. + public void SkipJudge(ModelComparisonSeriesJudgeStatus status) + { + this.JudgeChoice = null; + this.JudgeReasoning = string.Empty; + this.JudgeStatus = status; + } + + /// + /// Gets the number of the chat in which a model of the series answered. + /// + /// The first or the second model of the series. + /// 1 for the first chat, 2 for the second one. + public int ChatNumberOf(ModelComparisonModelChoice model) => (model is ModelComparisonModelChoice.FIRST) == this.FirstModelIsChatOne ? 1 : 2; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTaskState.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTaskState.cs new file mode 100644 index 000000000..f69cffc2c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTaskState.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How far a task of a comparison series got. +/// +public enum ModelComparisonSeriesTaskState +{ + /// + /// The user works on the task in the first chat. + /// + FIRST_CHAT, + + /// + /// The user works on the task in the second chat, which started with the same first message. + /// + SECOND_CHAT, + + /// + /// The user is done with both chats and decides which one helped more. + /// + DECIDING, + + /// + /// The user decided; the task counts toward the result of the series. + /// + DONE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTexts.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTexts.cs new file mode 100644 index 000000000..685028c35 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesTexts.cs @@ -0,0 +1,35 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The long explanations of the mode Everyday Tasks of the Model Comparison assistant, which its page and the dialog of a new +/// comparison series share. +/// +public static class ModelComparisonSeriesTexts +{ + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(ModelComparisonSeriesTexts).Namespace, nameof(ModelComparisonSeriesTexts)); + + /// + /// What a comparison series is. + /// + public static string WhatASeriesIs => TA("A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series."); + + /// + /// How a task of a series runs, step by step. + /// + public static string HowATaskWorks => TA("A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind."); + + /// + /// How the two models of a series are chosen and kept blind. + /// + public static string WhatTheModelsAreFor => TA("Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series."); + + /// + /// What the judge of a series does, why it is useful, and what it needs. + /// + public static string WhatTheJudgeIsFor => TA("After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task."); + + /// + /// What both models of a series share, and what decides what the selection offers. + /// + public static string WhatBothModelsShare => TA("Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them."); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesThreads.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesThreads.cs new file mode 100644 index 000000000..f152fcbe5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesThreads.cs @@ -0,0 +1,81 @@ +using AIStudio.Chat; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Creates the chats of a comparison series and readies them for a request. +/// +/// +/// Both chats of a task get the same setup: the profile, the tools, and the data sources of the series, and the same +/// system prompt, which asks the model not to name itself. A model which says what it is would end the blindness of the +/// comparison. The series names the tools itself, so the chat runs them without a visible selection of its own. +/// +public static class ModelComparisonSeriesThreads +{ + /// + /// The system prompt of every chat of a comparison series, the same for both models. + /// + public const string SYSTEM_PROMPT = $""" + {SystemPrompts.DEFAULT} + + The user compares you with another AI model without knowing which of the two answers. Do not state your name, the name of your model, or the company which made you, and do not hint at them. If the user asks, say that you cannot tell during the comparison. + """; + + /// + /// Creates a chat of a series, without any messages. + /// + /// The series. + /// The name of the chat, for the log and an export. + /// The chat; it gets its ID from the caller. + public static ChatThread Create(ModelComparisonSeries series, string name) => new() + { + WorkspaceId = Guid.Empty, + Name = name, + IncludeDateTime = true, + SystemPrompt = SYSTEM_PROMPT, + SelectedProfile = series.ProfileId, + SelectedChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE.Id, + SelectedToolIds = [.. series.ToolIds], + DataSourceOptions = DataSourceOptionsOf(series), + Blocks = [], + }; + + /// + /// Readies a chat of a series for the next request: what a chat does not store, and what the series decides anew. + /// + /// The chat. + /// The series. + /// The provider which answers. + /// The tools of the series which the provider may use in this chat, see ToolRegistry.FilterToolIdsForProvider. + public static void PrepareForRequest(ChatThread thread, ModelComparisonSeries series, AIStudio.Settings.Provider provider, IEnumerable toolIds) + { + thread.SystemPrompt = SYSTEM_PROMPT; + thread.SystemPromptOrigin = null; + thread.SelectedProvider = provider.Id; + thread.SelectedProfile = series.ProfileId; + thread.SelectedChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE.Id; + thread.IncludeDateTime = true; + thread.SelectedToolIds = [.. series.ToolIds]; + thread.DataSourceOptions = DataSourceOptionsOf(series); + thread.RuntimeAssistantName = string.Empty; + thread.RuntimeSelectedToolIds = [.. toolIds]; + thread.RuntimeToolsAreAssistantManaged = true; + } + + /// + /// Gets how a chat of a series searches its data sources: those of the series, in the way the series chose for + /// both chats, without an automatic selection or validation which could treat the models differently. + /// + /// The series. + /// The options. + private static DataSourceOptions DataSourceOptionsOf(ModelComparisonSeries series) => new() + { + DisableDataSources = series.DataSourceIds.Count == 0, + AutomaticDataSourceSelection = false, + AutomaticValidation = false, + RetrievalMode = series.RetrievalMode ?? DataSourceRetrievalMode.SEMANTIC_SEARCH, + PreselectedDataSourceIds = [.. series.DataSourceIds], + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor new file mode 100644 index 000000000..5fa8402d3 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor @@ -0,0 +1,48 @@ +@inherits MSGComponentBase + + + + + + @string.Format(ModelComparisonFormat.Culture, T("Judge on task {0}"), this.TaskNumber) + + + + @switch (this.SeriesTask.JudgeStatus) + { + case ModelComparisonSeriesJudgeStatus.VERDICT: + + @this.VerdictText + + + break; + + case ModelComparisonSeriesJudgeStatus.TOO_LONG: + + @T("Both chats together are too long for the judge, so it did not judge this task.") + + break; + + case ModelComparisonSeriesJudgeStatus.UNAVAILABLE: + + @T("The judge is no longer available as it was when the series began, so it did not judge this task.") + + break; + + case ModelComparisonSeriesJudgeStatus.NOT_ASKED or ModelComparisonSeriesJudgeStatus.NO_VERDICT when this.IsJudging: + + + @T("The judge reads both chats. This can take a while.") + + break; + + default: + + @(this.SeriesTask.JudgeStatus is ModelComparisonSeriesJudgeStatus.NO_VERDICT ? T("The judge did not give a usable verdict.") : T("The judge has not judged this task yet.")) + + + @T("Ask the judge") + + break; + } + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor.cs new file mode 100644 index 000000000..c4156a359 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesVerdict.razor.cs @@ -0,0 +1,68 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the judge of a comparison series said about a decided task, or why it said nothing. +/// +/// +/// Shown only after the decision of the user, so that the judge cannot anchor it. The judge names the chats as the +/// user saw them, "Chat 1" and "Chat 2", never a model. +/// +public partial class ModelComparisonSeriesVerdict : MSGComponentBase +{ + /// + /// Gets or sets the decided task. + /// + [Parameter, EditorRequired] + public ModelComparisonSeriesTask SeriesTask { get; set; } = null!; + + /// + /// Gets or sets the number of the task in its series, from 1. + /// + [Parameter, EditorRequired] + public int TaskNumber { get; set; } + + /// + /// Gets or sets whether the judge reads the chats of the task right now. + /// + [Parameter] + public bool IsJudging { get; set; } + + /// + /// Gets or sets what happens when the user asks the judge again, e.g., after a judge without a usable verdict. + /// + [Parameter] + public EventCallback AskJudge { get; set; } + + /// + /// Names the chat the judge preferred, and says whether that matches the decision of the user. + /// + private string VerdictText + { + get + { + var vote = this.SeriesTask.Vote; + if (this.SeriesTask.JudgeChoice is not { } judgeChoice) + return string.Empty; + + if (judgeChoice is ModelComparisonModelChoice.TIE) + return vote is ModelComparisonModelChoice.TIE + ? TA("The judge found both chats equally helpful, as you did.") + : TA("The judge found both chats equally helpful, while you preferred one of them."); + + var chatNumber = this.SeriesTask.ChatNumberOf(judgeChoice); + var text = vote switch + { + ModelComparisonModelChoice.TIE => TA("The judge preferred chat {0}, while you found both chats equally helpful."), + { } chosen when chosen == judgeChoice => TA("The judge preferred chat {0}, as you did."), + + _ => TA("The judge preferred chat {0}, while you preferred the other one."), + }; + + return string.Format(ModelComparisonFormat.Culture, text, chatNumber); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor new file mode 100644 index 000000000..1c9520719 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor @@ -0,0 +1,114 @@ +@inherits MSGComponentBase + +@if (this.seriesIssue is { } issue) +{ + + @issue + +} + + + + @ModelComparisonSeriesTexts.HowATaskWorks + + + +@if (this.Series.OpenTask is { } task) +{ + + @* Stays at the top while the user scrolls through a long chat, like the header of the BeyondFacts Lab: which + task and chat this is, and the setup both models work with, which matters for the decision. Its background + is that of the paper below it. *@ +
+ + + @this.TaskHeading(task) + + + + @T("Discard task") + + + + +
+ + @if (task.CurrentChat is { } chat) + { + + @this.ChatHint(task) + + @if (this.allowedFileTypes is not null) + { + + @TA("You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual.") + + } + + @* Only once everything this chat needs is ready, see ShowOpenTaskAsync: *@ + @if (this.shownChat is { } shown && shown.ChatId == chat.ChatId) + { + + } + else + { + + } + + + @T("I'm done with this model") + + } + else + { + + } +
+} +else +{ + + @if (this.Series.Judge is not null && this.LastDecidedTask is { } decidedTask) + { + + } + + + @T("Setup of this series") + + + + + @this.NextTaskText + + + + @(this.DecidedTaskCount == 0 ? T("Start a task") : T("Test another task")) + + @if (this.DecidedTaskCount > 0) + { + + @(this.showsResult ? T("Hide the result") : T("Show the result")) + + } + + + + @if (this.showsResult && this.Series.IsResolved) + { + + + + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor.cs new file mode 100644 index 000000000..0b9b86f74 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSeriesView.razor.cs @@ -0,0 +1,496 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; +using AIStudio.Components; +using AIStudio.Dialogs; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Tools.Rust; +using AIStudio.Tools.ToolCallingSystem; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// An opened comparison series: the task the user works on, from the first chat over the second chat to the decision, +/// or the start of the next task. +/// +/// +/// The series is stored after every step, so the user continues at the same place after a restart, e.g., days later. +/// Which model answers in the first chat is drawn at random for every task. The second chat starts with a copy of the +/// first message of the first chat, which it sends by itself, see EmbeddedChat.OpeningMessage. +/// +/// Nothing on this view names a model before the result of the series is shown: messages about a provider leave out +/// its name, and a provider which was deleted or now uses another model only stops the series with a note that does +/// not say which one it was. +/// +public partial class ModelComparisonSeriesView : MSGComponentBase +{ + private const Tools.Components COMPONENT = Tools.Components.MODEL_COMPARISON_ASSISTANT; + + /// + /// What the user may attach when not both models can read images. + /// + private static readonly FileTypeFilter[] FILES_WITHOUT_IMAGES = [FileTypes.DOCUMENT, FileTypes.MAIL, FileTypes.AUDIO, FileTypes.VIDEO]; + + /// + /// Gets or sets the series. + /// + [Parameter, EditorRequired] + public ModelComparisonSeries Series { get; set; } = null!; + + [Inject] + private ModelComparisonStore ModelComparisonStore { get; init; } = null!; + + [Inject] + private EmbeddedChatService ChatService { get; init; } = null!; + + [Inject] + private ToolRegistry ToolRegistry { get; init; } = null!; + + [Inject] + private ModelComparisonSeriesJudge SeriesJudge { get; init; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + /// + /// The series which the fields below belong to, to notice when the parent shows another one. + /// + private ModelComparisonSeries? shownSeries; + + /// + /// Where the chats of the series live. + /// + private IEmbeddedChatStore chatStore = null!; + + /// + /// The provider of the first model of the series, or NONE when it is no longer available as it was. + /// + private AIStudio.Settings.Provider firstProvider = AIStudio.Settings.Provider.NONE; + + /// + /// The provider of the second model of the series, or NONE when it is no longer available as it was. + /// + private AIStudio.Settings.Provider secondProvider = AIStudio.Settings.Provider.NONE; + + /// + /// Why the user cannot work with the models of the series, or null when the user can. + /// + private string? seriesIssue; + + /// + /// What the user may attach, or null for every file a chat takes. Images only when both models can read them, so + /// that the second chat can start with the same first message. + /// + private FileTypeFilter[]? allowedFileTypes; + + /// + /// The chat the user works in right now, or null while the user decides or no task is open. + /// + private EmbeddedConversation? currentConversation; + + /// + /// The chat which shows, with its provider, or null while no chat shows. It is set together with everything else + /// the chat needs, once all of that is known, see ShowOpenTaskAsync. + /// + private ModelComparisonShownChat? shownChat; + + /// + /// Readies a request of the chat the user works in right now, with the provider of that chat. + /// + private Func> prepareChat = _ => Task.FromResult(true); + + /// + /// The copy of the first message of the first chat, with which the second chat starts, or null in the first chat. + /// + private ContentText? openingMessage; + + private bool isSaving; + + /// + /// Stops the judge when the user leaves the series: the series might be opened again meanwhile, and two copies of + /// it must not overwrite each other. The user can ask the judge again later. + /// + private readonly CancellationTokenSource judgeCancellation = new(); + + /// + /// Whether the judge reads the chats of a task right now. + /// + private bool isJudging; + + /// + /// Whether the result of the series shows, which names the models. Shown from the start once the user saw it. + /// + private bool showsResult; + + /// + /// Whether the explanation of how a task works shows. Open until the user decided the first task of the series. + /// + private bool explanationExpanded; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + this.ChatService.ConversationChanged += this.OnConversationChangedAsync; + await base.OnInitializedAsync(); + } + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + if (ReferenceEquals(this.shownSeries, this.Series)) + return; + + this.shownSeries = this.Series; + this.showsResult = this.Series.IsResolved; + this.explanationExpanded = this.DecidedTaskCount == 0; + this.chatStore = new ModelComparisonSeriesChatStore(this.ModelComparisonStore, this.Series.Id); + this.CheckModels(); + await this.ShowOpenTaskAsync(); + } + + protected override void DisposeResources() + { + this.ChatService.ConversationChanged -= this.OnConversationChangedAsync; + this.judgeCancellation.Cancel(); + this.judgeCancellation.Dispose(); + base.DisposeResources(); + } + + #endregion + + /// + /// Looks up the providers of both models and checks whether the user may still compare them. + /// + private void CheckModels() + { + this.firstProvider = ModelComparisonProviderCheck.ProviderAsItWas(this.SettingsManager, this.Series.FirstProviderId, this.Series.First); + this.secondProvider = ModelComparisonProviderCheck.ProviderAsItWas(this.SettingsManager, this.Series.SecondProviderId, this.Series.Second); + + if (this.firstProvider.UsedLLMProvider is LLMProviders.NONE || this.secondProvider.UsedLLMProvider is LLMProviders.NONE) + this.seriesIssue = TA("One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew."); + else + this.seriesIssue = ModelComparisonProviderCheck.IssueOf(this.SettingsManager, this.firstProvider) ?? ModelComparisonProviderCheck.IssueOf(this.SettingsManager, this.secondProvider); + + this.allowedFileTypes = this.firstProvider.SupportsImageInput() && this.secondProvider.SupportsImageInput() ? null : FILES_WITHOUT_IMAGES; + } + + /// + /// Shows the task the user works on, as far as it got, and what the chat of that step needs. + /// + /// + /// No chat shows until its provider and its first message are known: the view renders while this method waits, + /// and also while the series is stored right before, when the task already is in its next chat. A chat shown then + /// would load without the first message, which it sends only when it opens, or would send it to the model of the + /// previous chat. The markup therefore shows a chat only when shownChat is the current chat of the task. + /// + private async Task ShowOpenTaskAsync() + { + this.shownChat = null; + this.currentConversation = null; + this.openingMessage = null; + if (this.Series.OpenTask is not { } task || ModelComparisonShownChat.Of(task, this.firstProvider, this.secondProvider) is not { } chat) + return; + + ContentText? firstMessage = null; + if (chat.OpensWithFirstMessage) + { + var firstChat = await this.ConversationOf(task.ChatOne.ChatId); + firstMessage = firstChat.FirstUserMessage?.Content?.DeepClone() as ContentText; + } + + var conversation = await this.ConversationOf(chat.ChatId); + var provider = chat.Provider; + this.prepareChat = thread => this.PrepareAsync(thread, provider); + this.openingMessage = firstMessage; + this.currentConversation = conversation; + this.shownChat = chat; + } + + /// + /// Gets a chat of the series from the service which runs it. + /// + /// The ID of the chat. + private Task ConversationOf(Guid chatId) => this.ChatService.GetAsync(this.chatStore, COMPONENT, chatId, this.CreateThread); + + /// + /// Creates a chat of the series, for a chat which has no stored thread yet. + /// + private ChatThread CreateThread() => ModelComparisonSeriesThreads.Create(this.Series, this.Series.Name); + + /// + /// Readies a request of a chat of the series, see ModelComparisonSeriesThreads.PrepareForRequest. + /// + /// The chat. + /// The provider which answers in the chat. + /// Always true: whether the provider may answer, the embedded chat checks itself. + private Task PrepareAsync(ChatThread thread, AIStudio.Settings.Provider provider) + { + var toolIds = this.ToolRegistry.FilterToolIdsForProvider(provider, this.Series.ToolIds, thread.RequiredOutboundDataRestriction.Restriction); + ModelComparisonSeriesThreads.PrepareForRequest(thread, this.Series, provider, toolIds); + return Task.FromResult(true); + } + + /// + /// Renders again when the chat the user works in changed, e.g., once an answer is complete. + /// + /// The ID of the chat which changed. + private Task OnConversationChangedAsync(Guid chatId) => this.currentConversation?.ChatId == chatId + ? this.InvokeAsync(this.StateHasChanged) + : Task.CompletedTask; + + /// + /// Whether the user may be done with the current chat: the model answered at least once, and no answer runs. + /// + private bool CanFinishChat => this.currentConversation is { IsRunning: false, HasAnswer: true } && !this.isSaving; + + private int DecidedTaskCount => this.Series.Tasks.Count(task => task.State is ModelComparisonSeriesTaskState.DONE); + + private ModelComparisonSeriesTask? LastDecidedTask => this.Series.Tasks.LastOrDefault(task => task.State is ModelComparisonSeriesTaskState.DONE); + + private string NextTaskText => this.DecidedTaskCount == 0 + ? TA("Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first.") + : string.Format(ModelComparisonFormat.Culture, TA("Decided tasks so far: {0}. Do you want to test another task?"), this.DecidedTaskCount); + + private string TaskHeading(ModelComparisonSeriesTask task) + { + var number = this.Series.Tasks.IndexOf(task) + 1; + var text = task.State switch + { + ModelComparisonSeriesTaskState.FIRST_CHAT => T("Task {0}: chat 1"), + ModelComparisonSeriesTaskState.SECOND_CHAT => T("Task {0}: chat 2"), + + _ => TA("Task {0}: your decision"), + }; + + return string.Format(ModelComparisonFormat.Culture, text, number); + } + + private string ChatHint(ModelComparisonSeriesTask task) => task.State is ModelComparisonSeriesTaskState.FIRST_CHAT + ? TA("Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it.") + : TA("Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more."); + + /// + /// Begins a new task, with a model drawn at random for the first chat. Once the user saw the result, it asks each + /// time: the user knows the models then, so the task is no longer blind. + /// + private async Task StartTaskAsync() + { + if (this.seriesIssue is not null || this.Series.OpenTask is not null) + return; + + if (this.Series.IsResolved && !await this.ConfirmTaskWhichIsNotBlindAsync()) + return; + + var task = ModelComparisonSeriesTask.Begin(DateTimeOffset.UtcNow, Random.Shared.Next(2) == 0); + this.Series.Tasks.Add(task); + if (!await this.SaveAsync()) + { + this.Series.Tasks.Remove(task); + return; + } + + await this.ShowOpenTaskAsync(); + } + + /// + /// Asks the user whether to test another task in a series whose result they saw. + /// + /// True when the user wants to. + private async Task ConfirmTaskWhichIsNotBlindAsync() + { + var parameters = new DialogParameters + { + { dialog => dialog.Message, TA("You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway?") }, + }; + + var reference = await this.DialogService.ShowAsync(T("Test another task"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + return result is { Canceled: false }; + } + + /// + /// Ends the work in the current chat: after the first chat, the second one begins; after the second one, the user + /// decides. + /// + private async Task FinishChatAsync() + { + if (!this.CanFinishChat || this.Series.OpenTask is not { } task || this.currentConversation is not { FirstUserMessage: { } firstMessage } conversation) + return; + + var startedAtUtc = firstMessage.Time.ToUniversalTime(); + var finishedAtUtc = DateTimeOffset.UtcNow; + var isFinished = task.State is ModelComparisonSeriesTaskState.FIRST_CHAT + ? task.FinishFirstChat(startedAtUtc, finishedAtUtc, conversation.UserMessageCount, ModelComparisonQuestionPreview.Of((firstMessage.Content as ContentText)?.Text ?? string.Empty)) + : task.FinishSecondChat(startedAtUtc, finishedAtUtc, conversation.UserMessageCount); + + if (!isFinished) + return; + + // Shown even when saving fails: the series is saved again with the next step. + await this.SaveAsync(); + await this.ShowOpenTaskAsync(); + } + + /// + /// Takes the decision of the user, and then asks the judge, so that its verdict cannot anchor the decision. + /// + /// The chat the user chose: COLUMN_A for the first chat, COLUMN_B for the second one. + private async Task DecideAsync(ModelComparisonVote vote) + { + if (this.Series.OpenTask is not { } task || !task.Decide(vote, DateTimeOffset.UtcNow, !this.Series.IsResolved)) + return; + + await this.SaveAsync(); + await this.ShowOpenTaskAsync(); + await this.AskJudgeAsync(task); + } + + /// + /// Asks the judge of the series about a decided task and stores its verdict. + /// + /// The decided task. + private async Task AskJudgeAsync(ModelComparisonSeriesTask task) + { + if (this.isJudging || this.Series.Judge is null || task.State is not ModelComparisonSeriesTaskState.DONE) + return; + + this.isJudging = true; + this.StateHasChanged(); + try + { + var chatOne = await this.ConversationOf(task.ChatOne.ChatId); + var chatTwo = await this.ConversationOf(task.ChatTwo.ChatId); + await this.SeriesJudge.JudgeAsync(this.Series, task, chatOne.Thread, chatTwo.Thread, ModelComparisonMode.TASKS.PageTitle, this.judgeCancellation.Token); + } + catch (OperationCanceledException) + { + // The user left the series; the task waits for the judge until the user asks again. + return; + } + finally + { + this.isJudging = false; + } + + await this.SaveAsync(); + } + + /// + /// Shows or hides the result of the series. The first time, it asks: the result names the models, which ends the + /// blind phase of the series for good. + /// + private async Task ToggleResultAsync() + { + if (this.showsResult || this.Series.IsResolved) + { + this.showsResult = !this.showsResult; + return; + } + + var parameters = new DialogParameters + { + { dialog => dialog.Message, TA("The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result?") }, + }; + + var reference = await this.DialogService.ShowAsync(T("Show the result"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled) + return; + + this.Series.IsResolved = true; + this.Series.ResolvedAtUtc = DateTimeOffset.UtcNow; + if (!await this.SaveAsync()) + { + this.Series.IsResolved = false; + this.Series.ResolvedAtUtc = null; + return; + } + + this.showsResult = true; + } + + /// + /// Discards the open task with both its chats, after asking. It does not count toward the result. + /// + private async Task DiscardTaskAsync() + { + if (this.Series.OpenTask is not { } task) + return; + + var parameters = new DialogParameters + { + { dialog => dialog.Message, TA("Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone.") }, + }; + + var reference = await this.DialogService.ShowAsync(T("Discard task"), parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled) + return; + + // An answer which still runs would otherwise store its chat again: + await this.ChatService.ForgetAsync(task.ChatOne.ChatId); + await this.ChatService.ForgetAsync(task.ChatTwo.ChatId); + + var index = this.Series.Tasks.IndexOf(task); + this.Series.Tasks.RemoveAt(index); + if (!await this.SaveAsync()) + { + this.Series.Tasks.Insert(index, task); + return; + } + + try + { + await this.ModelComparisonStore.DeleteSeriesChatAsync(this.Series.Id, task.ChatOne.ChatId); + await this.ModelComparisonStore.DeleteSeriesChatAsync(this.Series.Id, task.ChatTwo.ChatId); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // The series no longer knows the chats, so they only take some space: + this.Logger.LogWarning(exception, "Could not delete the chats of a discarded task of a comparison series."); + } + + await this.ShowOpenTaskAsync(); + } + + /// + /// Stores the series and tells the user when that fails. + /// + /// + /// A series the user deleted meanwhile, e.g., with all series in the results of all comparisons, stays deleted: + /// the page closes it, so there is nothing to tell. + /// + /// Whether the series was stored. + private async Task SaveAsync() + { + this.isSaving = true; + try + { + await this.ModelComparisonStore.SaveSeriesAsync(this.Series); + return true; + } + catch (FileNotFoundException) + { + this.Logger.LogInformation("A comparison series of the Model Comparison assistant was not stored, since the user deleted it meanwhile."); + return false; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not store a comparison series of the Model Comparison assistant."); + await this.MessageBus.SendError(new(AppIcons.SAVE_FAILED, TA("AI Studio could not save the comparison series. Please try again."))); + return false; + } + finally + { + this.isSaving = false; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonShownChat.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonShownChat.cs new file mode 100644 index 000000000..16c334d82 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonShownChat.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The chat of a task which the user works in right now, with what it needs before it shows. +/// +/// The ID of the chat. +/// The provider which answers in the chat. +/// Whether the chat starts with a copy of the first message of the first chat, which holds for the second chat. +public sealed record ModelComparisonShownChat(Guid ChatId, AIStudio.Settings.Provider Provider, bool OpensWithFirstMessage) +{ + /// + /// Tells which chat of a task the user works in, and which model answers there. + /// + /// The task. + /// The provider of the first model of the series. + /// The provider of the second model of the series. + /// The chat, or null while the user decides or once the task is done. + public static ModelComparisonShownChat? Of(ModelComparisonSeriesTask task, AIStudio.Settings.Provider firstProvider, AIStudio.Settings.Provider secondProvider) + { + if (task.CurrentChat is not { } chat) + return null; + + // The first model answers in the first chat when it was drawn for it, and in the second chat otherwise: + var isFirstChat = task.State is ModelComparisonSeriesTaskState.FIRST_CHAT; + var provider = isFirstChat == task.FirstModelIsChatOne ? firstProvider : secondProvider; + return new(chat.ChatId, provider, !isFirstChat); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSignificance.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSignificance.cs new file mode 100644 index 000000000..dcd68f22e --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSignificance.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Whether the runs decided so far say which of two models is better, see ModelComparisonWinShare. +/// +public enum ModelComparisonSignificance +{ + /// + /// Too few runs, or too even a result, to tell yet. + /// + NOT_YET_CLEAR, + + /// + /// The model wins clearly more often than it loses. + /// + CLEARLY_BETTER, + + /// + /// The model loses clearly more often than it wins. + /// + CLEARLY_WORSE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor new file mode 100644 index 000000000..3f1944940 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor @@ -0,0 +1,30 @@ +@inherits MSGComponentBase + + + @T("The median time each model took, so that a single slow answer does not distort the picture. Only complete answers count. The times also depend on the length of the answers and on how busy the servers were.") + +@if (this.timedSpeeds.Count > 0) +{ + +} + + + + @T("Model") + @T("Complete answers") + @T("Until the first token") + @T("Until the complete answer") + + + + @foreach (var speed in this.Speeds) + { + + @speed.Model.Label + @speed.AnswerCount.CompactCount() + @ModelComparisonFormat.Duration(speed.MedianFirstTokenTime) + @ModelComparisonFormat.Duration(speed.MedianTotalTime) + + } + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor.cs new file mode 100644 index 000000000..5271d8c20 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonSpeeds.razor.cs @@ -0,0 +1,50 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How fast the models answered: the median times as a chart, and as a table which also lists the models without a +/// complete answer. +/// +public partial class ModelComparisonSpeeds : MSGComponentBase +{ + /// + /// Gets or sets the speeds of all models. + /// + [Parameter, EditorRequired] + public IReadOnlyList Speeds { get; set; } = []; + + /// + /// The models with complete answers, whose median times the chart can show. + /// + private IReadOnlyList timedSpeeds = []; + + #region Overrides of ComponentBase + + protected override void OnParametersSet() + { + this.timedSpeeds = [.. this.Speeds.Where(speed => speed is { MedianFirstTokenTime: not null, MedianTotalTime: not null })]; + base.OnParametersSet(); + } + + #endregion + + /// + /// The median times of each model in seconds, as the two series of a bar chart. + /// + private List> SpeedSeries => + [ + new() { Name = T("Until the first token"), Data = new([.. this.timedSpeeds.Select(speed => speed.MedianFirstTokenTime!.Value.TotalSeconds)]) }, + new() { Name = T("Until the complete answer"), Data = new([.. this.timedSpeeds.Select(speed => speed.MedianTotalTime!.Value.TotalSeconds)]) }, + ]; + + private BarChartOptions ChartOptions => new() + { + ChartPalette = ModelComparisonCharts.SPEED_PALETTE, + YAxisTicks = ModelComparisonCharts.AxisStep(this.timedSpeeds.Select(speed => speed.MedianTotalTime!.Value.TotalSeconds)), + YAxisToStringFunc = ModelComparisonFormat.Seconds, + XAxisLabelRotation = ModelComparisonCharts.LabelRotation(this.timedSpeeds.Count), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatistics.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatistics.cs new file mode 100644 index 000000000..f402a2b19 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatistics.cs @@ -0,0 +1,94 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What all stored comparisons say about the models: which model wins, which wins against which, how often each judge +/// agrees with the user, and how fast each model answers. +/// +/// +/// A comparison of a model with itself, which tests how consistently it answers, says nothing about which model is +/// better: it counts toward the speed and the agreement of the judge, but not toward the rankings and the pairs. A +/// comparison series counts only with its blind tasks, and only once the user saw its result, see +/// ModelComparisonStatisticsBuilder.Add. +/// +public sealed record ModelComparisonStatistics +{ + /// + /// How many stored comparisons of answers there are. + /// + public int ComparisonCount { get; init; } + + /// + /// How many runs the stored comparisons of answers hold. + /// + public int RunCount { get; init; } + + /// + /// How many comparison series count, those whose result the user saw. + /// + public int SeriesCount { get; init; } + + /// + /// How many blind tasks the counted comparison series hold. + /// + public int TaskCount { get; init; } + + /// + /// Whether anything counts at all. + /// + public bool IsEmpty => this.ComparisonCount == 0 && this.SeriesCount == 0; + + /// + /// The models by the votes of the user, the one with the highest share of wins first. + /// + public IReadOnlyList VoteRanking { get; init; } = []; + + /// + /// The models by the verdicts of the judges, the one with the highest share of wins first. + /// + public IReadOnlyList JudgeRanking { get; init; } = []; + + /// + /// Every pair of models compared with each other, the one compared most often first. + /// + public IReadOnlyList Pairings { get; init; } = []; + + /// + /// Every judge, the one with the most compared runs first. + /// + public IReadOnlyList JudgeAgreements { get; init; } = []; + + /// + /// Every compared model, the fastest to complete an answer first. + /// + public IReadOnlyList Speeds { get; init; } = []; + + /// + /// Computes the statistics of the compared answers, one stored comparison at a time. + /// + /// The stored comparisons, see ModelComparisonStore.ReadResultsAsync. + /// The cancellation token. + /// The statistics. + public static Task ComputeAsync(IAsyncEnumerable records, CancellationToken token = default) => ComputeAsync(records, [], ModelComparisonResultSource.ANSWERS, token); + + /// + /// Computes the statistics of the results a source names, one stored comparison at a time. + /// + /// The stored comparisons of answers, see ModelComparisonStore.ReadResultsAsync; read only when the source names them. + /// The comparison series, see ModelComparisonStore.ListSeriesAsync. + /// Which results count. + /// The cancellation token. + /// The statistics. + public static async Task ComputeAsync(IAsyncEnumerable records, IEnumerable series, ModelComparisonResultSource source, CancellationToken token = default) + { + var builder = new ModelComparisonStatisticsBuilder(); + if (source is not ModelComparisonResultSource.TASKS) + await foreach (var record in records.WithCancellation(token)) + builder.Add(record); + + if (source is not ModelComparisonResultSource.ANSWERS) + foreach (var oneSeries in series) + builder.Add(oneSeries); + + return builder.Build(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatisticsBuilder.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatisticsBuilder.cs new file mode 100644 index 000000000..ceab9c079 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStatisticsBuilder.cs @@ -0,0 +1,307 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Folds stored comparisons into ModelComparisonStatistics, one at a time, so that never more than one of them is in +/// memory: only the counts and the times stay. +/// +internal sealed class ModelComparisonStatisticsBuilder +{ + /// + /// The identity of each model from its latest comparison, so that a renamed provider entry shows its new name. + /// + private readonly Dictionary latestIdentities = []; + + /// + /// When each model took part in its latest comparison. + /// + private readonly Dictionary latestTimes = []; + + private readonly Dictionary voteStandings = []; + + private readonly Dictionary judgeStandings = []; + + private readonly Dictionary pairings = []; + + private readonly Dictionary judgeAgreements = []; + + private readonly Dictionary> firstTokenTimes = []; + + private readonly Dictionary> totalTimes = []; + + private int comparisonCount; + + private int runCount; + + private int seriesCount; + + private int taskCount; + + /// + /// Counts a stored comparison of the stability mode. + /// + /// The stored comparison. + public void Add(ModelComparisonRecord record) + { + this.comparisonCount++; + this.runCount += record.Runs.Count; + this.AddComparison(record.First, record.Second, record.Judge, record.FinishedAtUtc); + + foreach (var run in record.Runs) + { + this.AddTimes(record.First.Key, run.First); + this.AddTimes(record.Second.Key, run.Second); + this.AddDecision(record.First, record.Second, record.Judge, run.Vote, run.JudgeChoice); + } + } + + /// + /// Counts the blind tasks of a comparison series whose result the user saw. A series before its result counts not + /// at all: its models must not show up anywhere with their names. A task decided after the result is not blind and + /// stays out, too. The tasks have no times of the answers, so they count toward neither speed. + /// + /// The series. + public void Add(ModelComparisonSeries series) + { + if (!series.IsResolved) + return; + + var blindTasks = series.Tasks.Where(task => task is { State: ModelComparisonSeriesTaskState.DONE, IsBlind: true }).ToList(); + if (blindTasks.Count == 0) + return; + + this.seriesCount++; + this.taskCount += blindTasks.Count; + this.AddComparison(series.First, series.Second, series.Judge, series.ResolvedAtUtc ?? series.LastActivityUtc); + + foreach (var task in blindTasks) + this.AddDecision(series.First, series.Second, series.Judge, task.Vote, task.JudgeChoice); + } + + /// + /// Gets the statistics of everything counted so far. + /// + /// The statistics. + public ModelComparisonStatistics Build() => new() + { + ComparisonCount = this.comparisonCount, + RunCount = this.runCount, + SeriesCount = this.seriesCount, + TaskCount = this.taskCount, + VoteRanking = this.Rank(this.voteStandings), + JudgeRanking = this.Rank(this.judgeStandings), + Pairings = + [ + .. this.pairings.Values + .Select(pairing => pairing with { First = this.latestIdentities[pairing.First.Key], Second = this.latestIdentities[pairing.Second.Key] }) + .OrderByDescending(pairing => pairing.ComparisonCount) + .ThenBy(pairing => pairing.First.Label, StringComparer.CurrentCultureIgnoreCase), + ], + JudgeAgreements = + [ + .. this.judgeAgreements + .Select(agreement => agreement.Value with { Judge = this.latestIdentities[agreement.Key] }) + .OrderByDescending(agreement => agreement.ComparedRuns) + .ThenBy(agreement => agreement.Judge.Label, StringComparer.CurrentCultureIgnoreCase), + ], + Speeds = + [ + .. this.totalTimes.Keys + .Select(key => new ModelComparisonModelSpeed + { + Model = this.latestIdentities[key], + AnswerCount = this.totalTimes[key].Count, + MedianFirstTokenTime = Median(this.firstTokenTimes[key]), + MedianTotalTime = Median(this.totalTimes[key]), + }) + .OrderBy(speed => speed.MedianTotalTime ?? TimeSpan.MaxValue) + .ThenBy(speed => speed.Model.Label, StringComparer.CurrentCultureIgnoreCase), + ], + }; + + /// + /// Counts a comparison of two models: remembers their names, and counts it for their pair. + /// + /// The first model of the comparison. + /// The second model of the comparison. + /// The judge, or null without one. + /// When the comparison was finished. + private void AddComparison(ModelComparisonModelIdentity first, ModelComparisonModelIdentity second, ModelComparisonModelIdentity? judge, DateTimeOffset finishedAtUtc) + { + this.Remember(first, finishedAtUtc); + this.Remember(second, finishedAtUtc); + if (judge is not null) + this.Remember(judge, finishedAtUtc); + + // A comparison of a model with itself tests how consistently it answers, not which model is better: + if (first.Key == second.Key) + return; + + var pair = ModelComparisonPairKey.Of(first.Key, second.Key); + var pairIsSwapped = pair.First != first.Key; + var pairing = this.pairings.GetValueOrDefault(pair) ?? new ModelComparisonPairing + { + First = pairIsSwapped ? second : first, + Second = pairIsSwapped ? first : second, + }; + + this.pairings[pair] = pairing with { ComparisonCount = pairing.ComparisonCount + 1 }; + } + + /// + /// Counts the vote of the user and the choice of the judge on one run or task. + /// + /// The first model of the comparison. + /// The second model of the comparison. + /// The judge, or null without one. + /// The vote of the user, or null without one. + /// The choice of the judge, or null without a usable verdict. + private void AddDecision(ModelComparisonModelIdentity first, ModelComparisonModelIdentity second, ModelComparisonModelIdentity? judge, ModelComparisonModelChoice? vote, ModelComparisonModelChoice? judgeChoice) + { + if (first.Key != second.Key) + { + Count(this.voteStandings, first, second, vote); + Count(this.judgeStandings, first, second, judgeChoice); + + var pair = ModelComparisonPairKey.Of(first.Key, second.Key); + var pairIsSwapped = pair.First != first.Key; + var pairing = this.pairings[pair]; + this.pairings[pair] = pairing with + { + Votes = pairing.Votes.Plus(Oriented(vote, pairIsSwapped)), + JudgeVerdicts = pairing.JudgeVerdicts.Plus(Oriented(judgeChoice, pairIsSwapped)), + }; + } + + if (judge is not null && vote is { } decidedVote && judgeChoice is { } decidedJudgeChoice) + { + var agreement = this.judgeAgreements.GetValueOrDefault(judge.Key) ?? new ModelComparisonJudgeAgreement { Judge = judge }; + this.judgeAgreements[judge.Key] = agreement with + { + Agreements = agreement.Agreements + (decidedVote == decidedJudgeChoice ? 1 : 0), + ComparedRuns = agreement.ComparedRuns + 1, + }; + } + } + + /// + /// Remembers the identity of a model when its comparison is the latest one so far. + /// + /// The identity in the stored comparison. + /// When the comparison was finished. + private void Remember(ModelComparisonModelIdentity identity, DateTimeOffset finishedAtUtc) + { + var key = identity.Key; + if (this.latestTimes.TryGetValue(key, out var latest) && finishedAtUtc < latest) + return; + + this.latestTimes[key] = finishedAtUtc; + this.latestIdentities[key] = identity; + } + + /// + /// Keeps the times of a complete answer. + /// + /// The key of the model. + /// How the model did in the run. + private void AddTimes(ModelComparisonModelKey model, ModelComparisonRecordAnswer answer) + { + if (!this.totalTimes.TryGetValue(model, out var totals)) + { + totals = []; + this.totalTimes[model] = totals; + this.firstTokenTimes[model] = []; + } + + // Only complete answers count: a failure or a cancellation would make a model look faster than it is. + if (!answer.Answered || answer.TotalTime is not { } totalTime) + return; + + totals.Add(totalTime); + if (answer.FirstTokenTime is { } firstTokenTime) + this.firstTokenTimes[model].Add(firstTokenTime); + } + + /// + /// Ranks the models, the one with the highest share of wins first; a model without decided runs comes last. + /// + /// The standings by model. + /// The ranking, with the latest name of each model. + private IReadOnlyList Rank(Dictionary standings) => + [ + .. standings + .Select(standing => standing.Value with { Model = this.latestIdentities[standing.Key] }) + .OrderByDescending(standing => standing.WinShare.Share ?? -1) + .ThenByDescending(standing => standing.WinShare.Decided) + .ThenBy(standing => standing.Model.Label, StringComparer.CurrentCultureIgnoreCase), + ]; + + /// + /// Counts a choice for and against both models of a comparison. + /// + /// The standings by model. + /// The first model of the comparison. + /// The second model of the comparison. + /// The choice of the run, or null without one. + private static void Count(Dictionary standings, ModelComparisonModelIdentity first, ModelComparisonModelIdentity second, ModelComparisonModelChoice? choice) + { + switch (choice) + { + case ModelComparisonModelChoice.FIRST: + Change(standings, first, standing => standing with { Wins = standing.Wins + 1 }); + Change(standings, second, standing => standing with { Losses = standing.Losses + 1 }); + break; + + case ModelComparisonModelChoice.SECOND: + Change(standings, first, standing => standing with { Losses = standing.Losses + 1 }); + Change(standings, second, standing => standing with { Wins = standing.Wins + 1 }); + break; + + case ModelComparisonModelChoice.TIE: + Change(standings, first, standing => standing with { Ties = standing.Ties + 1 }); + Change(standings, second, standing => standing with { Ties = standing.Ties + 1 }); + break; + } + } + + /// + /// Changes the standing of a model. + /// + /// The standings by model. + /// The model. + /// The change. + private static void Change(Dictionary standings, ModelComparisonModelIdentity model, Func change) + { + var standing = standings.GetValueOrDefault(model.Key) ?? new ModelComparisonModelStanding { Model = model }; + standings[model.Key] = change(standing); + } + + /// + /// Turns a choice in the order of a comparison into the order of its pair. + /// + /// The choice, in the order of the comparison. + /// Whether the pair names the models the other way round. + private static ModelComparisonModelChoice? Oriented(ModelComparisonModelChoice? choice, bool pairIsSwapped) => !pairIsSwapped ? choice : choice switch + { + ModelComparisonModelChoice.FIRST => ModelComparisonModelChoice.SECOND, + ModelComparisonModelChoice.SECOND => ModelComparisonModelChoice.FIRST, + + _ => choice, + }; + + /// + /// Gets the median of some times. + /// + /// The times. + /// The median, or null without any times. + internal static TimeSpan? Median(IReadOnlyCollection times) + { + if (times.Count == 0) + return null; + + var sorted = times.Order().ToList(); + var middle = sorted.Count / 2; + return sorted.Count % 2 == 1 + ? sorted[middle] + : (sorted[middle - 1] + sorted[middle]) / 2; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStorageOptions.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStorageOptions.cs new file mode 100644 index 000000000..4b5caaeec --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStorageOptions.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Overrides where the Model Comparison assistant stores its files, for tests. +/// +public sealed class ModelComparisonStorageOptions +{ + /// + /// Gets or initializes the folder in which the folder modelComparisons is created. + /// + public string? DataDirectory { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Presets.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Presets.cs new file mode 100644 index 000000000..e61e8a469 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Presets.cs @@ -0,0 +1,99 @@ +namespace AIStudio.Assistants.ModelComparison; + +public sealed partial class ModelComparisonStore +{ + /// + /// The folder of the presets, one file per preset. + /// + private const string PRESETS_DIRECTORY_NAME = "presets"; + + /// + /// Gets the folder of the presets. + /// + private string PresetsDirectory => Path.Combine(this.RootDirectory, PRESETS_DIRECTORY_NAME); + + /// + /// Gets the file of a preset. + /// + /// The ID of the preset. + private string PresetPath(Guid presetId) => Path.Combine(this.PresetsDirectory, $"{presetId:D}.json"); + + /// + /// Lists the presets, sorted by name. + /// + /// + /// A preset this version cannot read is left out, but stays on the disk, see ModelComparisonJson. + /// + /// The cancellation token. + /// The presets. + public async Task> ListPresetsAsync(CancellationToken token = default) + { + await this.InitializeAsync(token); + if (!Directory.Exists(this.PresetsDirectory)) + return []; + + List presets = []; + foreach (var path in Directory.EnumerateFiles(this.PresetsDirectory, "*.json")) + { + token.ThrowIfCancellationRequested(); + if (!Guid.TryParseExact(Path.GetFileNameWithoutExtension(path), "D", out var presetId)) + continue; + + // The ID inside the file must not lead to another file than the one it was read from: + if (await this.ReadJsonAsync(path, token) is { } preset) + presets.Add(preset with { Id = presetId }); + } + + return [.. presets.OrderBy(preset => preset.Name, StringComparer.CurrentCultureIgnoreCase)]; + } + + /// + /// Stores a preset, as a new one or in place of the one with the same ID. + /// + /// The preset. + /// The cancellation token. + /// The preset has no ID or no name. + public async Task SavePresetAsync(ModelComparisonPreset preset, CancellationToken token = default) + { + if (preset.Id == Guid.Empty) + throw new ArgumentException("A preset needs an ID.", nameof(preset)); + + if (string.IsNullOrWhiteSpace(preset.Name)) + throw new ArgumentException("A preset needs a name.", nameof(preset)); + + await this.InitializeAsync(token); + var gate = this.GetLock(preset.Id); + await gate.WaitAsync(token); + try + { + await WriteJsonAtomicAsync(this.PresetPath(preset.Id), preset with { Name = preset.Name.Trim() }, token); + } + finally + { + gate.Release(); + } + } + + /// + /// Deletes a preset. + /// + /// The ID of the preset. + /// The cancellation token. + public async Task DeletePresetAsync(Guid presetId, CancellationToken token = default) + { + await this.InitializeAsync(token); + var gate = this.GetLock(presetId); + await gate.WaitAsync(token); + try + { + var path = this.PresetPath(presetId); + if (File.Exists(path)) + File.Delete(path); + } + finally + { + gate.Release(); + this.ForgetLock(presetId); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Results.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Results.cs new file mode 100644 index 000000000..e82865c0d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Results.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; + +namespace AIStudio.Assistants.ModelComparison; + +public sealed partial class ModelComparisonStore +{ + /// + /// The folder of the stored comparisons, one file per batch. + /// + private const string RESULTS_DIRECTORY_NAME = "results"; + + /// + /// Gets the folder of the stored comparisons. + /// + private string ResultsDirectory => Path.Combine(this.RootDirectory, RESULTS_DIRECTORY_NAME); + + /// + /// Gets the file of a stored comparison. + /// + /// The ID of the batch. + private string ResultPath(Guid recordId) => Path.Combine(this.ResultsDirectory, $"{recordId:D}.json"); + + /// + /// Stores what the assistant keeps of a batch, in place of what it kept of the same batch before. + /// + /// The stored comparison. + /// The cancellation token. + /// The stored comparison has no ID. + public async Task SaveResultAsync(ModelComparisonRecord record, CancellationToken token = default) + { + if (record.Id == Guid.Empty) + throw new ArgumentException("A stored comparison needs an ID.", nameof(record)); + + await this.InitializeAsync(token); + var gate = this.GetLock(record.Id); + await gate.WaitAsync(token); + try + { + await WriteJsonAtomicAsync(this.ResultPath(record.Id), record, token); + } + finally + { + gate.Release(); + } + } + + /// + /// Reads the stored comparisons one by one, so that never more than one of them is in memory at once. + /// + /// + /// A file this version cannot read is skipped, but stays on the disk, see ModelComparisonJson. + /// + /// The cancellation token. + /// The stored comparisons, in no particular order. + public async IAsyncEnumerable ReadResultsAsync([EnumeratorCancellation] CancellationToken token = default) + { + await this.InitializeAsync(token); + if (!Directory.Exists(this.ResultsDirectory)) + yield break; + + foreach (var path in Directory.EnumerateFiles(this.ResultsDirectory, "*.json")) + { + token.ThrowIfCancellationRequested(); + if (!Guid.TryParseExact(Path.GetFileNameWithoutExtension(path), "D", out var recordId)) + continue; + + // The ID inside the file must not lead to another file than the one it was read from: + if (await this.ReadJsonAsync(path, token) is { } record) + yield return record with { Id = recordId }; + } + } + + /// + /// Deletes all stored comparisons, the presets aside. + /// + /// + /// Unlike reading, deleting takes the files this version cannot read along: the user asked for all results to go. + /// + /// The cancellation token. + public async Task DeleteAllResultsAsync(CancellationToken token = default) + { + await this.InitializeAsync(token); + if (!Directory.Exists(this.ResultsDirectory)) + return; + + foreach (var path in Directory.EnumerateFiles(this.ResultsDirectory, "*.json").ToList()) + { + token.ThrowIfCancellationRequested(); + if (!Guid.TryParseExact(Path.GetFileNameWithoutExtension(path), "D", out var recordId)) + continue; + + var gate = this.GetLock(recordId); + await gate.WaitAsync(token); + try + { + File.Delete(path); + } + finally + { + gate.Release(); + this.ForgetLock(recordId); + } + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Series.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Series.cs new file mode 100644 index 000000000..7d447679c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.Series.cs @@ -0,0 +1,305 @@ +using System.Text.Json; + +using AIStudio.Chat; + +namespace AIStudio.Assistants.ModelComparison; + +public sealed partial class ModelComparisonStore +{ + /// + /// The folder of the comparison series, one folder per series. + /// + private const string SERIES_DIRECTORY_NAME = "series"; + + /// + /// The file of a series in its folder. + /// + private const string SERIES_FILE_NAME = "series.json"; + + /// + /// The folder of the chats in the folder of a series. + /// + private const string CHATS_DIRECTORY_NAME = "chats"; + + /// + /// Gets the folder of all comparison series. + /// + private string AllSeriesDirectory => Path.Combine(this.RootDirectory, SERIES_DIRECTORY_NAME); + + /// + /// Gets the folder of a series. + /// + /// The ID of the series. + private string SeriesDirectory(Guid seriesId) => Path.Combine(this.AllSeriesDirectory, seriesId.ToString("D")); + + /// + /// Gets the file of a series. + /// + /// The ID of the series. + private string SeriesPath(Guid seriesId) => Path.Combine(this.SeriesDirectory(seriesId), SERIES_FILE_NAME); + + /// + /// Gets the file of a chat of a series. + /// + /// The ID of the series. + /// The ID of the chat. + private string SeriesChatPath(Guid seriesId, Guid chatId) => Path.Combine(this.SeriesDirectory(seriesId), CHATS_DIRECTORY_NAME, $"{chatId:D}.json"); + + /// + /// Lists the comparison series which this version can read, the one the user last worked on first. + /// + /// The cancellation token. + /// The series. + public async Task> ListSeriesAsync(CancellationToken token = default) + { + await this.InitializeAsync(token); + if (!Directory.Exists(this.AllSeriesDirectory)) + return []; + + List series = []; + foreach (var directory in Directory.EnumerateDirectories(this.AllSeriesDirectory)) + { + token.ThrowIfCancellationRequested(); + if (Guid.TryParseExact(Path.GetFileName(directory), "D", out var seriesId) && await this.LoadSeriesAsync(seriesId, token) is { } loaded) + series.Add(loaded); + } + + return [.. series.OrderByDescending(entry => entry.LastActivityUtc)]; + } + + /// + /// Loads a comparison series. + /// + /// The ID of the series. + /// The cancellation token. + /// The series, or null when it does not exist or this version cannot read it. + public async Task LoadSeriesAsync(Guid seriesId, CancellationToken token = default) + { + await this.InitializeAsync(token); + + // The ID inside the file must not lead to another folder than the one it was read from: + return await this.ReadJsonAsync(this.SeriesPath(seriesId), token) is { } series + ? series with { Id = seriesId } + : null; + } + + /// + /// Stores a new comparison series. + /// + /// The series. + /// The cancellation token. + /// The series has no ID. + public async Task CreateSeriesAsync(ModelComparisonSeries series, CancellationToken token = default) + { + if (series.Id == Guid.Empty) + throw new ArgumentException("A comparison series needs an ID.", nameof(series)); + + await this.InitializeAsync(token); + var gate = this.GetLock(series.Id); + await gate.WaitAsync(token); + try + { + await WriteJsonAtomicAsync(this.SeriesPath(series.Id), series, token); + } + finally + { + gate.Release(); + } + } + + /// + /// Stores a comparison series which exists already, in place of what was stored of it before. + /// + /// + /// Guarded by the lock of the series, so that a series deleted meanwhile does not come back, e.g., with the verdict + /// of its judge. A new series goes through CreateSeriesAsync. + /// + /// The series. + /// The cancellation token. + /// The series has no ID. + /// The series does not exist, e.g., because the user deleted it. + public async Task SaveSeriesAsync(ModelComparisonSeries series, CancellationToken token = default) + { + if (series.Id == Guid.Empty) + throw new ArgumentException("A comparison series needs an ID.", nameof(series)); + + await this.InitializeAsync(token); + var gate = this.GetLock(series.Id); + await gate.WaitAsync(token); + try + { + var seriesPath = this.SeriesPath(series.Id); + if (!File.Exists(seriesPath)) + throw new FileNotFoundException("The comparison series does not exist.", seriesPath); + + await WriteJsonAtomicAsync(seriesPath, series, token); + } + finally + { + gate.Release(); + } + } + + /// + /// Deletes a comparison series together with its chats. + /// + /// The ID of the series. + /// The cancellation token. + public async Task DeleteSeriesAsync(Guid seriesId, CancellationToken token = default) + { + await this.InitializeAsync(token); + var gate = this.GetLock(seriesId); + await gate.WaitAsync(token); + try + { + var directory = this.SeriesDirectory(seriesId); + if (Directory.Exists(directory)) + Directory.Delete(directory, recursive: true); + } + finally + { + gate.Release(); + this.ForgetLock(seriesId); + } + } + + /// + /// Deletes all comparison series together with their chats, the presets and the results of the mode Answer + /// Variance aside. + /// + /// + /// Unlike listing, deleting takes the series this version cannot read along: the user asked for all series to go. + /// + /// The cancellation token. + public async Task DeleteAllSeriesAsync(CancellationToken token = default) + { + await this.InitializeAsync(token); + if (!Directory.Exists(this.AllSeriesDirectory)) + return; + + foreach (var directory in Directory.EnumerateDirectories(this.AllSeriesDirectory).ToList()) + { + token.ThrowIfCancellationRequested(); + if (Guid.TryParseExact(Path.GetFileName(directory), "D", out var seriesId)) + await this.DeleteSeriesAsync(seriesId, token); + } + } + + /// + /// Loads a chat of a comparison series. + /// + /// The ID of the series. + /// The ID of the chat. + /// The cancellation token. + /// The chat, or null when it has no file yet. + /// This version cannot open the chat; the file stays as it is. + public async Task LoadSeriesChatAsync(Guid seriesId, Guid chatId, CancellationToken token = default) + { + await this.InitializeAsync(token); + var path = this.SeriesChatPath(seriesId, chatId); + if (!File.Exists(path)) + return null; + + ChatThread? thread; + try + { + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true); + thread = await JsonSerializer.DeserializeAsync(stream, WorkspaceBehaviour.JSON_OPTIONS, token); + } + catch (JsonException exception) + { + throw new InvalidDataException("This version of AI Studio cannot open the chat.", exception); + } + + return thread ?? throw new InvalidDataException("This version of AI Studio cannot open the chat."); + } + + /// + /// Stores a chat of a comparison series, in place of what was stored of it before. + /// + /// + /// Guarded by the lock of the series, so that a series deleted meanwhile does not come back with a chat alone. + /// + /// The ID of the series. + /// The chat, with its ID. + /// The cancellation token. + /// The series does not exist, e.g., because the user deleted it while the model answered. + public async Task SaveSeriesChatAsync(Guid seriesId, ChatThread thread, CancellationToken token = default) + { + await this.InitializeAsync(token); + var gate = this.GetLock(seriesId); + await gate.WaitAsync(token); + try + { + var seriesPath = this.SeriesPath(seriesId); + if (!File.Exists(seriesPath)) + throw new FileNotFoundException("The comparison series of the chat does not exist.", seriesPath); + + await WriteJsonAtomicAsync(this.SeriesChatPath(seriesId, thread.ChatId), thread, token, WorkspaceBehaviour.JSON_OPTIONS); + } + finally + { + gate.Release(); + } + } + + /// + /// Deletes a chat of a comparison series, e.g., because the user discarded its task. + /// + /// The ID of the series. + /// The ID of the chat. + /// The cancellation token. + public async Task DeleteSeriesChatAsync(Guid seriesId, Guid chatId, CancellationToken token = default) + { + await this.InitializeAsync(token); + var gate = this.GetLock(seriesId); + await gate.WaitAsync(token); + try + { + var path = this.SeriesChatPath(seriesId, chatId); + if (File.Exists(path)) + File.Delete(path); + } + finally + { + gate.Release(); + } + } + + /// + /// Reads which chats of comparison series lie on the disk, from the folders alone, e.g., for the cleanup of the + /// provenance store at the start. + /// + /// The ID of the series of every chat, by the ID of the chat. + public IReadOnlyDictionary ReadAllSeriesChatLocations() + { + var locations = new Dictionary(); + if (!Directory.Exists(this.AllSeriesDirectory)) + return locations; + + foreach (var directory in Directory.EnumerateDirectories(this.AllSeriesDirectory)) + { + var chatsDirectory = Path.Combine(directory, CHATS_DIRECTORY_NAME); + if (!Guid.TryParseExact(Path.GetFileName(directory), "D", out var seriesId) || !Directory.Exists(chatsDirectory)) + continue; + + foreach (var path in Directory.EnumerateFiles(chatsDirectory, "*.json")) + if (Guid.TryParseExact(Path.GetFileNameWithoutExtension(path), "D", out var chatId)) + locations[chatId] = seriesId; + } + + return locations; + } + + /// + /// Tells when the file of a chat of a comparison series was last written. + /// + /// The ID of the series. + /// The ID of the chat. + /// The time, or null when there is no file. + public DateTimeOffset? GetSeriesChatWrittenAt(Guid seriesId, Guid chatId) + { + var path = this.SeriesChatPath(seriesId, chatId); + return File.Exists(path) ? new DateTimeOffset(File.GetLastWriteTimeUtc(path)) : null; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.cs new file mode 100644 index 000000000..22b523a54 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonStore.cs @@ -0,0 +1,163 @@ +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; + +using AIStudio.Settings; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Stores the files of the Model Comparison assistant below the data directory. +/// +/// +/// The store keeps no paths: it derives every path from IDs, so a backup works on another computer as well. It writes +/// each file atomically, guards each file with a lock of its own, and never deletes anything on its own; only the user +/// deletes. A file this version cannot read is left alone rather than overwritten or removed. +/// +/// The logger. +/// Overrides the data directory, for tests. +public sealed partial class ModelComparisonStore(ILogger logger, ModelComparisonStorageOptions? storageOptions = null) +{ + /// + /// The name of the folder below the data directory. + /// + public const string ROOT_DIRECTORY_NAME = "modelComparisons"; + + /// + /// The JSON options of all files of the assistant. + /// + private static readonly JsonSerializerOptions JSON_OPTIONS = ModelComparisonJson.PERSISTENCE; + + /// + /// The lock of each file, by the ID which names it. + /// + private readonly ConcurrentDictionary fileLocks = []; + + /// + /// Serializes the first start of the store. + /// + private readonly SemaphoreSlim initializationLock = new(1, 1); + + /// + /// Whether the store cleaned up already. + /// + private bool initialized; + + /// + /// Gets the folder of the assistant below the data directory. + /// + private string RootDirectory => Path.Combine( + storageOptions?.DataDirectory ?? + SettingsManager.DataDirectory ?? + throw new InvalidOperationException("The AI Studio data directory is not initialized."), + ROOT_DIRECTORY_NAME); + + /// + /// Reads a file of the store. + /// + /// The type of the file. + /// The file. + /// The cancellation token. + /// What the file holds, or null when it does not exist or this version cannot read it. + private async Task ReadJsonAsync(string path, CancellationToken token) where T : class + { + if (!File.Exists(path)) + return null; + + try + { + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true); + return await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) + { + logger.LogWarning(exception, "Could not read the file '{Path}' of the Model Comparison assistant.", path); + return null; + } + } + + /// + /// Writes a file of the store so that readers see either the old or the new content, never a part of it. + /// + /// The type of the file. + /// The file to write. + /// The new content. + /// The cancellation token. + /// The JSON options, those of the assistant by default; a chat takes those of the workspaces. + private static async Task WriteJsonAtomicAsync(string targetPath, T value, CancellationToken token, JsonSerializerOptions? options = null) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); + var temporaryPath = $"{targetPath}.tmp-{Guid.NewGuid():N}"; + try + { + await File.WriteAllTextAsync(temporaryPath, JsonSerializer.Serialize(value, options ?? JSON_OPTIONS), new UTF8Encoding(false), token); + await using (var stream = new FileStream(temporaryPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, 4_096, true)) + await stream.FlushAsync(token); + + File.Move(temporaryPath, targetPath, overwrite: true); + } + finally + { + TryDeleteFile(temporaryPath); + } + } + + /// + /// Deletes a file when it exists, without failing. + /// + /// The file to delete. + private static void TryDeleteFile(string path) + { + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // Cleaning up a temporary file is best effort; the next start tries again. + } + } + + /// + /// Gets the lock of a file. + /// + /// The ID which names the file. + private SemaphoreSlim GetLock(Guid id) => this.fileLocks.GetOrAdd(id, _ => new(1, 1)); + + /// + /// Drops the lock of a file which does not exist anymore. + /// + /// + /// The semaphore is not disposed: another operation might still wait on it. + /// + /// The ID which named the file. + private void ForgetLock(Guid id) => this.fileLocks.TryRemove(id, out _); + + /// + /// Creates the folder of the assistant and removes temporary files a crash left behind. + /// + /// The cancellation token. + private async Task InitializeAsync(CancellationToken token) + { + if (this.initialized) + return; + + await this.initializationLock.WaitAsync(token); + try + { + if (this.initialized) + return; + + Directory.CreateDirectory(this.RootDirectory); + foreach (var temporaryPath in Directory.EnumerateFiles(this.RootDirectory, "*.tmp-*", SearchOption.AllDirectories)) + TryDeleteFile(temporaryPath); + + this.initialized = true; + } + finally + { + this.initializationLock.Release(); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTally.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTally.cs new file mode 100644 index 000000000..4b97c4e0f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTally.cs @@ -0,0 +1,64 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How often each model came out ahead across several runs: by the votes of the user, or by the verdicts of the judge. +/// +/// +/// Counted in models, never in columns: the column a model stood in was drawn fresh for every run, so only the model +/// stays comparable across runs. A run without a usable choice, such as one the user did not vote on, or one whose +/// judge gave no usable verdict, counts toward TotalRunCount, but toward nothing else. +/// +public sealed record ModelComparisonTally +{ + /// + /// How many runs there were, counted or not. + /// + public required int TotalRunCount { get; init; } + + public required int FirstModelWins { get; init; } + + public required int SecondModelWins { get; init; } + + public required int Ties { get; init; } + + /// + /// How many runs had a usable choice and were counted. + /// + public int ScoredRunCount => this.FirstModelWins + this.SecondModelWins + this.Ties; + + /// + /// How many runs had no usable choice and were left out. + /// + public int UnscoredRunCount => this.TotalRunCount - this.ScoredRunCount; + + /// + /// The tally of no runs at all. + /// + public static readonly ModelComparisonTally NONE = new() + { + TotalRunCount = 0, + FirstModelWins = 0, + SecondModelWins = 0, + Ties = 0, + }; + + /// + /// Counts the choices of several runs. + /// + /// The choice of each run, already turned from a column into a model; null for a run without a usable choice. + /// The tally. + public static ModelComparisonTally Of(IEnumerable choices) => choices.Aggregate(NONE, (tally, choice) => tally.Plus(choice)); + + /// + /// Counts one more run. + /// + /// The choice of the run, already turned from a column into a model; null for a run without a usable choice. + /// The tally with the run. + public ModelComparisonTally Plus(ModelComparisonModelChoice? choice) => this with + { + TotalRunCount = this.TotalRunCount + 1, + FirstModelWins = this.FirstModelWins + (choice is ModelComparisonModelChoice.FIRST ? 1 : 0), + SecondModelWins = this.SecondModelWins + (choice is ModelComparisonModelChoice.SECOND ? 1 : 0), + Ties = this.Ties + (choice is ModelComparisonModelChoice.TIE ? 1 : 0), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTexts.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTexts.cs new file mode 100644 index 000000000..6be1a5eaa --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonTexts.cs @@ -0,0 +1,34 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The long explanations of the mode Answer Variance of the Model Comparison assistant. +/// +public static class ModelComparisonTexts +{ + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(ModelComparisonTexts).Namespace, nameof(ModelComparisonTexts)); + + /// + /// How the two models are compared, and what comparing a model with itself shows. + /// + public static string WhatTheModelsAreFor => TA("Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again."); + + /// + /// What several runs are good for, and what they cost. + /// + public static string WhatTheRunsAreFor => TA("Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge."); + + /// + /// What the two models receive, and what they do not, unlike in the chat. + /// + public static string WhatTheModelsReceive => TA("Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”."); + + /// + /// What the material is, and where it comes from. + /// + public static string WhatTheMaterialIs => TA("The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own."); + + /// + /// What the judge does, why it is useful, and what it costs. + /// + public static string WhatTheJudgeIsFor => TA("A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run."); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVote.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVote.cs new file mode 100644 index 000000000..ef89c21cd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVote.cs @@ -0,0 +1,15 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// What the voter chose while the answers were still shown without the names of the models. +/// +/// +/// This names a column on the screen, not a model. Which model stood in a column is what +/// ModelComparisonPresentationOrder says. +/// +public enum ModelComparisonVote +{ + COLUMN_A, + COLUMN_B, + TIE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor new file mode 100644 index 000000000..503478fd9 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor @@ -0,0 +1,44 @@ +@inherits MSGComponentBase + +@* From the breakpoint md on, both columns stand in the same row, which keeps them equally high, and the question with + its buttons follows below: the button of each column under it, the tie between them. In a narrower window, the + question comes first, and each column is followed by its own button, with a line between the two columns. The + order classes arrange both from the same markup. *@ + + @if (!string.IsNullOrWhiteSpace(this.Heading)) + { + + + @this.Heading + + + } + + + @this.ColumnA + + + + @this.ALabel + + + + + + + + + @this.ColumnB + + + + @this.BLabel + + + + + + @this.TieLabel + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor.cs new file mode 100644 index 000000000..eb902dd52 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoteColumns.razor.cs @@ -0,0 +1,64 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// Two columns to compare, with the button of each below it and the tie between them: the answers of a run in the +/// mode Answer Variance, and the chats of a task in the mode Everyday Tasks. +/// +/// +/// Both modes share this layout, so that the user votes the same way in both: the button which chooses a column stands +/// below it, in a narrow window as well, where the second column moves below the first one. +/// +public partial class ModelComparisonVoteColumns : MSGComponentBase +{ + /// + /// Gets or sets what the left column shows, e.g., the first answer. + /// + [Parameter, EditorRequired] + public RenderFragment ColumnA { get; set; } = null!; + + /// + /// Gets or sets what the right column shows, e.g., the second answer. + /// + [Parameter, EditorRequired] + public RenderFragment ColumnB { get; set; } = null!; + + /// + /// Gets or sets the question above the buttons, or an empty text without one. + /// + [Parameter] + public string Heading { get; set; } = string.Empty; + + /// + /// Gets or sets the text of the button which chooses the left column. + /// + [Parameter, EditorRequired] + public string ALabel { get; set; } = string.Empty; + + /// + /// Gets or sets the text of the button which chooses the right column. + /// + [Parameter, EditorRequired] + public string BLabel { get; set; } = string.Empty; + + /// + /// Gets or sets the text of the button which finds both columns equally good. + /// + [Parameter, EditorRequired] + public string TieLabel { get; set; } = string.Empty; + + /// + /// Gets or sets whether the buttons are disabled, e.g., while the vote is saved. + /// + [Parameter] + public bool Disabled { get; set; } + + /// + /// Gets or sets what happens when the user votes: COLUMN_A for the left column, COLUMN_B for the right one. + /// + [Parameter] + public EventCallback Vote { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor new file mode 100644 index 000000000..1473c0d5f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor @@ -0,0 +1,49 @@ +@inherits MSGComponentBase + +@if (this.Entry.Result.BothCompleted) +{ + + @* Until the batch is over, nothing on this screen may say which model wrote which answer: no name, no time. A small model is usually faster, so a time would give the model away. *@ + + @this.Heading + + + + @TA("You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end.") + + + + + + + + + + + + @* Below every other button in both layouts, so outside the columns: *@ + @if (this.Batch.RunCount > 1) + { + + + @T("Skip this run") + + + @T("Skip remaining votes") + + + } + +} +else +{ + + + @T("At least one of the models did not answer in this run, so there is nothing to compare.") + + + + @(this.Batch.IsLastEntry ? T("Finish") : T("Next run")) + + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor.cs new file mode 100644 index 000000000..29136dd19 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonVoting.razor.cs @@ -0,0 +1,52 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.ModelComparison; + +/// +/// The current run of a batch, while the user votes: both answers without the names of the models, in random columns, +/// and the buttons of the vote. A run in which a model did not answer offers only to move on. +/// +public partial class ModelComparisonVoting : MSGComponentBase +{ + /// + /// Gets or sets the batch. + /// + [Parameter, EditorRequired] + public ModelComparisonBatch Batch { get; set; } = null!; + + /// + /// Gets or sets the current run of the batch. + /// + [Parameter, EditorRequired] + public ModelComparisonBatchEntry Entry { get; set; } = null!; + + /// + /// Gets or sets what happens when the user votes. + /// + [Parameter] + public EventCallback Vote { get; set; } + + /// + /// Gets or sets what happens when the user skips the vote on this run. + /// + [Parameter] + public EventCallback SkipThisRun { get; set; } + + /// + /// Gets or sets what happens when the user skips the votes on all remaining runs. + /// + [Parameter] + public EventCallback SkipRemainingVotes { get; set; } + + /// + /// Gets or sets what happens when the user moves past a run which has nothing to vote on. + /// + [Parameter] + public EventCallback NextEntry { get; set; } + + private string Heading => this.Batch.RunCount > 1 + ? string.Format(ModelComparisonFormat.Culture, T("Run {0} of {1}"), this.Batch.CurrentIndex + 1, this.Batch.RunCount) + : T("Answers"); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonWinShare.cs b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonWinShare.cs new file mode 100644 index 000000000..ed7943818 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ModelComparison/ModelComparisonWinShare.cs @@ -0,0 +1,95 @@ +namespace AIStudio.Assistants.ModelComparison; + +/// +/// How often a model won the runs which were decided, together with how sure that share is. +/// +/// +/// Only decided runs count: a tie says that neither model is better, not that both are. The share comes with the +/// Wilson score interval at 95 %, which stays meaningful for a handful of runs and for shares near 0 or 1, unlike the +/// textbook interval. Only when the whole interval lies above one half is the model clearly better, and only when it +/// lies below one half clearly worse; everything else is not yet clear. +/// +/// The interval treats every run as an independent test. The runs of one batch ask the same question, though, so they +/// are less independent than that, and the interval is narrower than it should be. That is why the statistics count +/// runs, but show how many comparisons they come from as well. +/// +public sealed record ModelComparisonWinShare +{ + /// + /// The quantile of the standard normal distribution for a two-sided interval of 95 %. + /// + private const double Z_95 = 1.959964; + + /// + /// How many decided runs the model won. + /// + public required int Wins { get; init; } + + /// + /// How many decided runs the model lost. + /// + public required int Losses { get; init; } + + /// + /// How many runs were decided. + /// + public int Decided => this.Wins + this.Losses; + + /// + /// The share of the decided runs which the model won, or null when no run was decided. + /// + public double? Share { get; private init; } + + /// + /// The lower end of the interval, or null when no run was decided. + /// + public double? LowerBound { get; private init; } + + /// + /// The upper end of the interval, or null when no run was decided. + /// + public double? UpperBound { get; private init; } + + /// + /// Whether the decided runs say which model is better. + /// + public ModelComparisonSignificance Significance { get; private init; } + + /// + /// Computes the share and its interval. + /// + /// How many decided runs the model won. + /// How many decided runs the model lost. + /// The share. + public static ModelComparisonWinShare Of(int wins, int losses) + { + ArgumentOutOfRangeException.ThrowIfNegative(wins); + ArgumentOutOfRangeException.ThrowIfNegative(losses); + + var decided = wins + losses; + if (decided == 0) + return new() { Wins = 0, Losses = 0, Significance = ModelComparisonSignificance.NOT_YET_CLEAR }; + + var share = (double)wins / decided; + var zSquared = Z_95 * Z_95; + var denominator = 1 + zSquared / decided; + var center = share + zSquared / (2.0 * decided); + var margin = Z_95 * Math.Sqrt(share * (1 - share) / decided + zSquared / (4.0 * decided * decided)); + var lower = (center - margin) / denominator; + var upper = (center + margin) / denominator; + + return new() + { + Wins = wins, + Losses = losses, + Share = share, + LowerBound = lower, + UpperBound = upper, + Significance = lower > 0.5 + ? ModelComparisonSignificance.CLEARLY_BETTER + : upper < 0.5 + ? ModelComparisonSignificance.CLEARLY_WORSE + : ModelComparisonSignificance.NOT_YET_CLEAR, + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor index 5fb01441f..06178f115 100644 --- a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor +++ b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor @@ -15,7 +15,8 @@ } else { - + @* Bound by its value, the field shows no issue before you type, also when you switch the live translation off: *@ + } diff --git a/app/MindWork AI Studio/Chat/ChatTranscript.cs b/app/MindWork AI Studio/Chat/ChatTranscript.cs new file mode 100644 index 000000000..bb8a35454 --- /dev/null +++ b/app/MindWork AI Studio/Chat/ChatTranscript.cs @@ -0,0 +1,79 @@ +using System.Text; + +namespace AIStudio.Chat; + +/// +/// Writes what the user sees of a chat as plain text, for a model which reads the chat, such as a judge. +/// +/// +/// Only the messages the user sees count: hidden requests, the results of tools, and the content of attached files +/// stay out. A call of a tool is one line with its name and its arguments, each shortened, so that a reader learns what +/// the model looked up without reading everything it got back. The thinking of a model stays out as well. +/// +public static class ChatTranscript +{ + /// + /// How many characters of each argument of a tool call the transcript keeps. + /// + public const int MAX_ARGUMENT_LENGTH = 80; + + /// + /// Writes the transcript of a chat. + /// + /// The chat. + /// The messages in their order, each below a line naming who wrote it. + public static string Of(ChatThread thread) + { + var transcript = new StringBuilder(); + foreach (var block in thread.Blocks) + { + if (block is not { HideFromUser: false, Content: ContentText content }) + continue; + + var message = Message(block.Role, content); + if (message.Length == 0) + continue; + + if (transcript.Length > 0) + transcript.AppendLineLF(); + + transcript.AppendLineLF(block.Role is ChatRole.USER ? "[User]" : "[Assistant]"); + transcript.AppendLineLF(message); + } + + return transcript.ToString().TrimEnd(); + } + + /// + /// Writes one message: the calls of tools first, since the model made them before it answered, then the text, then + /// the names of attached files. + /// + private static string Message(ChatRole role, ContentText content) + { + var lines = new List(); + if (role is ChatRole.AI) + lines.AddRange(content.ToolInvocations.OrderBy(call => call.Order).Select(call => $"[Tool call: {call.ToolName}({Arguments(call.Arguments)})]")); + + var text = role is ChatRole.AI ? content.Text.RemoveThinkTags().Trim() : content.Text.Trim(); + if (text.Length > 0) + lines.Add(text); + + if (content.FileAttachments.Count > 0) + lines.Add($"[Attached files: {string.Join(", ", content.FileAttachments.Select(attachment => attachment.FileName))}]"); + + return string.Join('\n', lines); + } + + private static string Arguments(IReadOnlyDictionary arguments) => string.Join(", ", arguments.Select(argument => $"{argument.Key}={Shortened(argument.Value)}")); + + private static string Shortened(string value) + { + var singleLine = value.ReplaceLineEndings(" ").Trim(); + if (singleLine.Length <= MAX_ARGUMENT_LENGTH) + return singleLine; + + // Cutting between the two halves of a character outside the basic plane would leave half of it: + var length = char.IsHighSurrogate(singleLine[MAX_ARGUMENT_LENGTH - 1]) ? MAX_ARGUMENT_LENGTH - 1 : MAX_ARGUMENT_LENGTH; + return singleLine[..length] + "…"; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index 82a46ebda..b45eb61ec 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -573,7 +573,7 @@ public sealed class ContentText : IContent var modelLoadResult = await provider.GetTextModels(token: token); if (!modelLoadResult.Success) { - var userMessage = modelLoadResult.FailureReason.ToUserMessage(provider.InstanceName); + var userMessage = modelLoadResult.FailureReason.ToUserMessage(provider.NameForUser); if (!string.IsNullOrWhiteSpace(userMessage)) await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, userMessage)); @@ -598,7 +598,7 @@ public sealed class ContentText : IContent { var emptyModelsMessage = string.Format( TB("We could load models from '{0}', but the provider did not return any usable text models."), - provider.InstanceName); + provider.NameForUser); await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, emptyModelsMessage)); LOGGER.LogWarning("Skipping AI request because there are no models available from '{ProviderInstanceName}' (provider={ProviderType}).", provider.InstanceName, provider.Provider); @@ -610,9 +610,9 @@ public sealed class ContentText : IContent var message = string.Format( TA("The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings."), - chatModel.Id, - provider.InstanceName, - provider.Provider); + provider.ModelNameForUser(chatModel), + provider.NameForUser, + provider.TypeForUser); await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, message)); LOGGER.LogWarning("Skipping AI request because model '{ModelId}' is not available from '{ProviderInstanceName}' (provider={ProviderType}).", chatModel.Id, provider.InstanceName, provider.Provider); diff --git a/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatOpening.cs b/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatOpening.cs new file mode 100644 index 000000000..706ed85cb --- /dev/null +++ b/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatOpening.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Chat.Embedded; + +/// +/// Decides when an embedded chat sends the message it opens with, see EmbeddedChat.OpeningMessage. +/// +public static class EmbeddedChatOpening +{ + /// + /// Tells whether an embedded chat sends its opening message now. + /// + /// + /// The message may arrive after the chat loaded, e.g., when the parent learns it only then: it is sent all the + /// same, as long as the user wrote nothing in the chat meanwhile. It is sent once per chat, also when sending + /// fails and it waits in the field instead. + /// + /// The message the chat opens with, or null without one. + /// The chat shown, or null while it loads. + /// Whether the chat sent its opening message already. + /// True when the chat sends the message now. + public static bool SendsNow(ContentText? openingMessage, EmbeddedConversation? conversation, bool wasSent) => + openingMessage is not null && !wasSent && conversation is { IsAvailable: true, IsRunning: false, UserMessageCount: 0 }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatRequest.cs b/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatRequest.cs new file mode 100644 index 000000000..e166317ea --- /dev/null +++ b/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatRequest.cs @@ -0,0 +1,39 @@ +namespace AIStudio.Chat.Embedded; + +/// +/// A message for an embedded chat, together with what the assistant decides about the request it starts. +/// +public sealed record EmbeddedChatRequest +{ + /// + /// The provider which answers. + /// + public required AIStudio.Settings.Provider Provider { get; init; } + + /// + /// The message, with its attachments. The chat stores it as it is. + /// + public required ContentText Message { get; init; } + + /// + /// Whether the user does not see the message, e.g., a request which lets the model open a conversation. + /// + public bool IsHidden { get; init; } + + /// + /// Whether messages to the user leave out which provider and which model answer, see IProvider.HidesIdentity. + /// + public bool HidesModelIdentity { get; init; } + + /// + /// Readies the chat for the request before the message goes in: its system prompt, profile, tools, and data + /// sources, which a chat partly does not store. Returns false to send nothing; it tells the user why itself. + /// + public Func> PrepareAsync { get; init; } = _ => Task.FromResult(true); + + /// + /// Gets the chat which the request sends, e.g., with images which the stored chat never holds. The chat itself + /// by default. + /// + public Func ThreadForRequest { get; init; } = thread => thread; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatService.cs b/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatService.cs new file mode 100644 index 000000000..7e8924df6 --- /dev/null +++ b/app/MindWork AI Studio/Chat/Embedded/EmbeddedChatService.cs @@ -0,0 +1,336 @@ +using System.Collections.Concurrent; + +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Tools.AssistantSessions; + +namespace AIStudio.Chat.Embedded; + +/// +/// Runs the chats which assistants show on their own pages, such as the two chats of a task of the Model Comparison +/// assistant. +/// +/// +/// A singleton, so that an answer goes on while the user looks at another page: the page asks for the chat again when +/// it comes back and listens to ConversationChanged. Each request registers in AssistantSessionService, which shows the +/// work in the navigation. An embedded chat lives in a store of its assistant, never in a workspace, see +/// IEmbeddedChatStore. What a request carries beside the conversation, such as the system prompt or the tools, the +/// assistant decides for every request, see EmbeddedChatRequest. +/// +/// The settings. +/// The registry of running assistant sessions. +/// The message bus, for errors. +/// The logger. +/// Lets the model answer a request; asks the provider by default, and is replaced in tests. +public sealed class EmbeddedChatService(SettingsManager settingsManager, AssistantSessionService sessionService, MessageBus messageBus, ILogger logger, Func? answerAsync = null) +{ + /// + /// How often a streamed answer renders at most. + /// + private static readonly TimeSpan STREAMING_RENDER_MIN_TIME = TimeSpan.FromMilliseconds(100); + + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(EmbeddedChatService).Namespace, nameof(EmbeddedChatService)); + + /// + /// The chats loaded so far, by their IDs. + /// + private readonly ConcurrentDictionary conversations = new(); + + /// + /// Serializes loading a chat, so that two pages never load the same chat twice. + /// + private readonly SemaphoreSlim loadLock = new(1, 1); + + /// + /// Guards whether a chat answers right now, so that two requests never run in the same chat. + /// + private readonly Lock runningLock = new(); + + private readonly Func answerAsync = answerAsync ?? AnswerWithProviderAsync; + + /// + /// Happens whenever a chat changed: a message, a chunk of an answer, or the end of a request. It names the ID of + /// the chat. + /// + /// + /// Called on the thread of the request. A page renders through InvokeAsync. + /// + public event Func? ConversationChanged; + + /// + /// Gets a chat, loading it on the first call. + /// + /// Where the chat lives. + /// The assistant which shows the chat. + /// The ID of the chat. + /// Creates the thread of a chat which has none stored yet; it gets the ID of the chat. + /// The cancellation token. + /// The chat; it is not available when this version cannot open the stored one. + public async Task GetAsync(IEmbeddedChatStore store, Tools.Components component, Guid chatId, Func createThread, CancellationToken token = default) + { + if (this.conversations.TryGetValue(chatId, out var conversation)) + return conversation; + + await this.loadLock.WaitAsync(token); + try + { + if (this.conversations.TryGetValue(chatId, out conversation)) + return conversation; + + ChatThread? thread; + try + { + thread = await store.LoadThreadAsync(chatId, token); + if (thread is null) + { + thread = createThread(); + thread.ChatId = chatId; + } + } + catch (InvalidDataException exception) + { + // The file stays as it is, see IEmbeddedChatStore.LoadThreadAsync: + logger.LogWarning(exception, "Could not open the embedded chat '{ChatId}' of the component '{Component}'.", chatId, component); + thread = null; + } + + conversation = new EmbeddedConversation { ChatId = chatId, Component = component, Store = store, Thread = thread }; + this.conversations[chatId] = conversation; + return conversation; + } + finally + { + this.loadLock.Release(); + } + } + + /// + /// Sends a message and lets the model answer it. + /// + /// + /// The message is stored before the model answers, so that a crash loses at most the answer. What goes wrong is + /// reported through the message bus; the method itself does not fail. + /// + /// The ID of a chat which GetAsync loaded before. + /// The message and what the request carries beside it. + /// True when the message went into the chat, even when the model could not answer it. + public async Task SendAsync(Guid chatId, EmbeddedChatRequest request) + { + if (string.IsNullOrWhiteSpace(request.Message.Text) || !this.conversations.TryGetValue(chatId, out var conversation) || conversation.Thread is not { } thread) + return false; + + lock (this.runningLock) + { + if (conversation.IsRunning) + return false; + + conversation.IsRunning = true; + } + + var messageIsStored = false; + ContentBlock? answerBlock = null; + var sessionKey = SessionKey(conversation); + var sessionId = Guid.Empty; + var status = AssistantSessionStatus.FAILED; + try + { + if (!await request.PrepareAsync(thread) || !await this.CheckProviderAsync(conversation, thread, request.Provider)) + return false; + + thread.RuntimeComponent = conversation.Component; + thread.Blocks.Add(new ContentBlock + { + Time = DateTimeOffset.Now, + ContentType = ContentType.TEXT, + Role = ChatRole.USER, + Content = request.Message, + HideFromUser = request.IsHidden, + }); + + await conversation.Store.SaveThreadAsync(thread); + messageIsStored = true; + + var answer = new ContentText { InitialRemoteWait = true }; + answerBlock = new ContentBlock + { + Time = DateTimeOffset.Now, + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = answer, + }; + + var lastRendering = DateTimeOffset.MinValue; + answer.StreamingEvent = async () => + { + var now = DateTimeOffset.Now; + if (now - lastRendering < STREAMING_RENDER_MIN_TIME) + return; + + lastRendering = now; + await this.NotifyAsync(chatId); + }; + + answer.StreamingDone = () => this.NotifyAsync(chatId); + thread.Blocks.Add(answerBlock); + + var cancellation = new CancellationTokenSource(); + sessionId = (await sessionService.TryBeginAsync(sessionKey, conversation.Component.Name, cancellation, thread, new(StringComparer.Ordinal))).SessionId; + await this.NotifyAsync(chatId); + + await this.answerAsync(request, answer, request.ThreadForRequest(thread), cancellation.Token); + status = cancellation.IsCancellationRequested ? AssistantSessionStatus.CANCELED : AssistantSessionStatus.COMPLETED; + } + catch (OperationCanceledException) + { + status = AssistantSessionStatus.CANCELED; + } + catch (ProviderRequestException exception) + { + logger.LogError(exception, "The provider request of an embedded chat failed. Status={StatusCode}, Reason='{ReasonPhrase}'.", exception.StatusCode, exception.ReasonPhrase); + await messageBus.SendError(new(AppIcons.PROVIDER_ERROR, exception.UserMessage)); + } + catch (FileNotFoundException) + { + // What the chat belonged to was deleted before the message went in; nothing is left to store. + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) + { + logger.LogError(exception, "Could not store the embedded chat '{ChatId}'.", chatId); + await messageBus.SendError(new(AppIcons.SAVE_FAILED, TA("AI Studio could not save this conversation. Please try again."))); + } + finally + { + await this.FinishAsync(conversation, thread, answerBlock, sessionKey, sessionId, status); + } + + return messageIsStored; + } + + /// + /// Stops the answer the model writes in a chat. + /// + /// The ID of the chat. + public Task CancelAsync(Guid chatId) => this.conversations.TryGetValue(chatId, out var conversation) + ? sessionService.CancelAsync(SessionKey(conversation)) + : Task.CompletedTask; + + /// + /// Stops the answer of a chat and forgets the chat, e.g., because the user deleted what it belonged to. + /// + /// The ID of the chat. + public async Task ForgetAsync(Guid chatId) + { + await this.CancelAsync(chatId); + this.conversations.TryRemove(chatId, out _); + } + + /// + /// Lets the provider of a request answer it. + /// + /// The request. + /// The answer, which the stream fills. + /// The chat which the request sends. + /// The cancellation token. + private static async Task AnswerWithProviderAsync(EmbeddedChatRequest request, ContentText answer, ChatThread thread, CancellationToken token) + { + var provider = request.Provider.CreateProvider(); + provider.HidesIdentity = request.HidesModelIdentity; + await answer.CreateFromProviderAsync(provider, request.Provider.Model, request.Message, thread, token); + } + + /// + /// Checks whether a provider may answer in a chat, and tells the user why not. + /// + /// The chat. + /// The thread of the chat, readied for the request. + /// The provider. + /// True when the provider may answer. + private async Task CheckProviderAsync(EmbeddedConversation conversation, ChatThread thread, AIStudio.Settings.Provider provider) + { + string? reason = null; + if (provider.UsedLLMProvider is LLMProviders.NONE) + reason = TA("Please select a provider."); + else if (!settingsManager.IsProviderConfident(provider, conversation.Component) || !thread.IsLLMProviderAllowed(provider)) + reason = TA("The selected provider may not see this conversation. Please select another provider."); + + if (reason is null) + return true; + + await messageBus.SendError(new(AppIcons.PROVIDER_ERROR, reason)); + return false; + } + + /// + /// Ends a request: removes an empty answer, stores the chat, and closes the session. + /// + /// The chat. + /// The thread of the chat. + /// The block of the answer, or null when the request did not get that far. + /// The key of the session. + /// The ID of the session, or Guid.Empty when none began. + /// How the request ended. + private async Task FinishAsync(EmbeddedConversation conversation, ChatThread thread, ContentBlock? answerBlock, AssistantSessionKey sessionKey, Guid sessionId, AssistantSessionStatus status) + { + if (answerBlock is { Content: ContentText answer } && string.IsNullOrWhiteSpace(answer.Text)) + thread.Blocks.Remove(answerBlock); + + if (answerBlock is not null) + { + try + { + await conversation.Store.SaveThreadAsync(thread); + } + catch (FileNotFoundException) + { + // What the chat belonged to was deleted while the model answered; nothing is left to store. + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) + { + logger.LogError(exception, "Could not store the embedded chat '{ChatId}'.", conversation.ChatId); + await messageBus.SendError(new(AppIcons.SAVE_FAILED, TA("AI Studio could not save this conversation. Please try again."))); + } + } + + lock (this.runningLock) + conversation.IsRunning = false; + + if (sessionId != Guid.Empty) + { + await sessionService.CompleteAsync(sessionKey, sessionId, status, string.Empty, thread, new(StringComparer.Ordinal)); + + // The assistant keeps the chat itself; the registry only shows that the assistant is at work: + await sessionService.ClearAsync(sessionKey); + } + + await this.NotifyAsync(conversation.ChatId); + } + + /// + /// Tells every page which listens that a chat changed. + /// + /// The ID of the chat. + private async Task NotifyAsync(Guid chatId) + { + if (this.ConversationChanged is not { } handlers) + return; + + foreach (var handler in handlers.GetInvocationList().Cast>()) + { + try + { + await handler(chatId); + } + catch (Exception exception) + { + // A page which went away meanwhile must not stop the answer: + logger.LogDebug(exception, "A page could not show a change of an embedded chat."); + } + } + } + + /// + /// Gets the key under which a request of a chat registers in AssistantSessionService. + /// + /// The chat. + private static AssistantSessionKey SessionKey(EmbeddedConversation conversation) => new(conversation.Component, conversation.ChatId.ToString("D")); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/Embedded/EmbeddedConversation.cs b/app/MindWork AI Studio/Chat/Embedded/EmbeddedConversation.cs new file mode 100644 index 000000000..ff599e6fb --- /dev/null +++ b/app/MindWork AI Studio/Chat/Embedded/EmbeddedConversation.cs @@ -0,0 +1,57 @@ +namespace AIStudio.Chat.Embedded; + +/// +/// An embedded chat as EmbeddedChatService keeps it, with whether the model answers in it right now. +/// +public sealed class EmbeddedConversation +{ + /// + /// The ID of the chat. + /// + public required Guid ChatId { get; init; } + + /// + /// The assistant which shows the chat. Its requests register in AssistantSessionService under it. + /// + public required Tools.Components Component { get; init; } + + /// + /// Where the chat lives. + /// + public required IEmbeddedChatStore Store { get; init; } + + /// + /// The thread of the chat, or null when this version cannot open the stored one. + /// + public ChatThread? Thread { get; internal set; } + + /// + /// Whether the model answers in the chat right now. + /// + public bool IsRunning { get; internal set; } + + /// + /// Whether the chat could be opened. + /// + public bool IsAvailable => this.Thread is not null; + + /// + /// Whether the chat holds any message the user sees. + /// + public bool HasVisibleMessages => this.Thread?.Blocks.Any(block => !block.HideFromUser) ?? false; + + /// + /// How many messages the user wrote in the chat, those the user does not see left aside. + /// + public int UserMessageCount => this.Thread?.Blocks.Count(block => block is { Role: ChatRole.USER, HideFromUser: false }) ?? 0; + + /// + /// The first message of the user which the user sees, or null before it. + /// + public ContentBlock? FirstUserMessage => this.Thread?.Blocks.FirstOrDefault(block => block is { Role: ChatRole.USER, HideFromUser: false }); + + /// + /// Whether the chat holds an answer of the model which the user sees. + /// + public bool HasAnswer => this.Thread?.Blocks.Any(block => block is { Role: ChatRole.AI, HideFromUser: false }) ?? false; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/Embedded/IEmbeddedChatStore.cs b/app/MindWork AI Studio/Chat/Embedded/IEmbeddedChatStore.cs new file mode 100644 index 000000000..a44e34169 --- /dev/null +++ b/app/MindWork AI Studio/Chat/Embedded/IEmbeddedChatStore.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Chat.Embedded; + +/// +/// Where the embedded chats of an assistant live, e.g., in a folder of the assistant below the data directory. +/// +/// +/// An embedded chat never lives in a workspace: it belongs to the assistant which shows it, and the assistant decides +/// how long it stays. The store finds a chat by its ID, ChatThread.ChatId. +/// +public interface IEmbeddedChatStore +{ + /// + /// Loads a chat. + /// + /// The ID of the chat. + /// The cancellation token. + /// The chat, or null when it has no stored thread yet. + /// This version cannot open the stored chat; the file stays as it is. + public Task LoadThreadAsync(Guid chatId, CancellationToken token = default); + + /// + /// Stores a chat, in place of what was stored of it before. + /// + /// The chat, with its ID. + /// The cancellation token. + /// What the chat belonged to was deleted meanwhile; nothing is left to store. + public Task SaveThreadAsync(ChatThread thread, CancellationToken token = default); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantModeChoice.razor b/app/MindWork AI Studio/Components/AssistantModeChoice.razor new file mode 100644 index 000000000..e66793917 --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantModeChoice.razor @@ -0,0 +1,30 @@ +@typeparam TMode + + @foreach (var option in this.Options) + { + + + + + + + + @option.Title + + + + + + + @option.Description + + + + + @option.Title + + + + + } + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantModeChoice.razor.cs b/app/MindWork AI Studio/Components/AssistantModeChoice.razor.cs new file mode 100644 index 000000000..ba150fa9e --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantModeChoice.razor.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +/// +/// Shows one card per mode with which an assistant can start, and reports the mode the user chose. +/// +/// +/// The assistant shows nothing else until the user chose, so the texts of the options come from the assistant: they +/// tell what it does in each mode. Generic, so that every assistant names its modes with an enum of its own. +/// +/// The type of the modes of the assistant. +public partial class AssistantModeChoice : ComponentBase +{ + /// + /// The options to choose from, in the order the cards show them. + /// + [Parameter] + public IReadOnlyList> Options { get; set; } = []; + + /// + /// Called with the mode of the option the user chose. + /// + [Parameter] + public EventCallback ModeChosen { get; set; } + + private async Task Choose(AssistantModeOption option) => await this.ModeChosen.InvokeAsync(option.Mode); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantModeOption.cs b/app/MindWork AI Studio/Components/AssistantModeOption.cs new file mode 100644 index 000000000..4ad30c79f --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantModeOption.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Components; + +/// +/// One of the modes an assistant offers before it starts, e.g., to compare models on one request or on whole tasks. +/// +/// The type of the modes of the assistant. +/// The mode the assistant works in once the user chose this option. +/// The icon of the option. +/// The short name of the option. +/// What the assistant does in this mode. +public readonly record struct AssistantModeOption(TMode Mode, string Icon, string Title, string Description); \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/CompactExpansionPanel.razor b/app/MindWork AI Studio/Components/CompactExpansionPanel.razor index 8729a11eb..114c6bde5 100644 --- a/app/MindWork AI Studio/Components/CompactExpansionPanel.razor +++ b/app/MindWork AI Studio/Components/CompactExpansionPanel.razor @@ -1,5 +1,5 @@ - +
diff --git a/app/MindWork AI Studio/Components/CompactExpansionPanel.razor.cs b/app/MindWork AI Studio/Components/CompactExpansionPanel.razor.cs index fc548e3d9..4ce468b3a 100644 --- a/app/MindWork AI Studio/Components/CompactExpansionPanel.razor.cs +++ b/app/MindWork AI Studio/Components/CompactExpansionPanel.razor.cs @@ -34,6 +34,16 @@ public partial class CompactExpansionPanel : ComponentBase [Parameter] public EventCallback ExpandedChanged { get; set; } + /// + /// Whether the content stays rendered while the panel is collapsed. + /// + /// + /// By default, a collapsed panel drops its content, and a form around it no longer knows the fields inside. A panel + /// with fields which the form has to check also while it is collapsed sets this. + /// + [Parameter] + public bool KeepContentAlive { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor index 1b1c8e47a..dbe48a3ce 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor @@ -1,5 +1,5 @@ @inherits MSGComponentBase - + @if (this.GetProvider(providerData.Value) is { } provider) { @@ -10,4 +10,4 @@ @providerData.Name } - + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index f103e3961..d4a452f8a 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -33,8 +33,27 @@ public partial class ConfigurationProviderSelection : MSGComponentBase [Parameter] public Func IsLocked { get; set; } = () => false; + /// + /// The label of the option, or null for "Preselected provider". + /// + [Parameter] + public string? OptionDescription { get; set; } + + /// + /// The text of an entry which preselects no provider at all, or null to offer no such entry. + /// + /// + /// The entry stands before "Use app default" and selects the ID of Provider.NONE, because an empty text already + /// means the provider of the whole app. + /// + [Parameter] + public string? NoPreselectionText { get; set; } + private IEnumerable> FilteredData() { + if (this.NoPreselectionText is not null) + yield return new(this.NoPreselectionText, AIStudio.Settings.Provider.NONE.Id); + if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS) yield return new(T("Use app default"), string.Empty); @@ -70,6 +89,10 @@ public partial class ConfigurationProviderSelection : MSGComponentBase if(string.IsNullOrWhiteSpace(this.SelectedValue())) break; + + // Preselecting no provider stays valid, whichever providers exist: + if (this.NoPreselectionText is not null && this.SelectedValue() == AIStudio.Settings.Provider.NONE.Id) + break; // Check if the selected value is still valid: if (this.Data.All(x => x.Value != this.SelectedValue())) diff --git a/app/MindWork AI Studio/Components/DataFolders.razor.cs b/app/MindWork AI Studio/Components/DataFolders.razor.cs index f51a85f08..bd492884f 100644 --- a/app/MindWork AI Studio/Components/DataFolders.razor.cs +++ b/app/MindWork AI Studio/Components/DataFolders.razor.cs @@ -83,6 +83,7 @@ public partial class DataFolders : MSGComponentBase DataBackupEntryKind.TOKENIZERS => TA("The tokenizers you chose for your providers."), DataBackupEntryKind.VISUAL_BRIEFINGS => TA("Your projects in the Visual Briefing Assistant."), DataBackupEntryKind.BEYOND_FACTS_LAB => TA("Your preparations in the BeyondFacts Lab, with their material and conversations."), + DataBackupEntryKind.MODEL_COMPARISONS => TA("Your presets, results, and comparison series with their chats in the Model Comparison assistant."), DataBackupEntryKind.DATA_SOURCE_INDEX => TA("The index of your local data sources: which files AI Studio embedded, and their text."), DataBackupEntryKind.VECTOR_STORES => TA("The embeddings of your local data sources."), DataBackupEntryKind.MEDIA_TRANSCRIPTS => TA("The transcripts of the audio and video files in your local data sources."), diff --git a/app/MindWork AI Studio/Components/DataSourceSelection.razor b/app/MindWork AI Studio/Components/DataSourceSelection.razor index f1db959cf..4e06fa965 100644 --- a/app/MindWork AI Studio/Components/DataSourceSelection.razor +++ b/app/MindWork AI Studio/Components/DataSourceSelection.razor @@ -70,10 +70,13 @@ } - + @if (this.AllowsAutomaticSelection) + { + + } @* With Semantic Search, no agent takes part: nothing would validate the retrieved data. *@ - @if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation && !this.IsSemanticSearchEffective) + @if (this.AllowsAutomaticSelection && this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation && !this.IsSemanticSearchEffective) { } diff --git a/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs b/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs index 1370b6f0b..93429ad51 100644 --- a/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs +++ b/app/MindWork AI Studio/Components/DataSourceSelection.razor.cs @@ -68,6 +68,42 @@ public partial class DataSourceSelection : MSGComponentBase [Parameter] public bool ConfiguresChatDefaults { get; set; } = true; + /// + /// Where the chats run which search these data sources. It decides, among other things, whether the models may + /// search the data sources themselves, see ToolRegistry.GetEffectiveRetrievalModeAsync. + /// + [Parameter] + public Tools.Components Component { get; set; } = Tools.Components.CHAT; + + /// + /// Further models which chat with the same data source options, e.g., the second model of a comparison series. + /// + /// + /// The selection then offers only the data sources each model may use, and Semantic Search only when each model + /// can search itself, see SharedSelectionRules. Without further models, it offers what LLMProvider alone may use. + /// + [Parameter] + public IReadOnlyList AdditionalProviders { get; set; } = []; + + /// + /// Further models which read what the data sources gave without searching them, e.g., the judge of a comparison + /// series, which reads both chats. Each of them must be allowed every data source the selection offers, but they + /// have no say in how the data sources are searched. + /// + [Parameter] + public IReadOnlyList AdditionalReaders { get; set; } = []; + + /// + /// Whether the user may let an AI select the data sources and validate what they gave. + /// + /// + /// Without, the user selects the data sources by hand, and the selection neither shows the switches for the AI nor + /// listens for the data sources an AI selected elsewhere: e.g., a comparison series which gives both models the + /// same data sources. Options which switch the AI on count as switched off then. + /// + [Parameter] + public bool AllowsAutomaticSelection { get; set; } = true; + [Inject] private DataSourceService DataSourceService { get; init; } = null!; @@ -97,13 +133,13 @@ public partial class DataSourceSelection : MSGComponentBase protected override async Task OnInitializedAsync() { - this.ApplyFilters([], [ Event.RAG_AUTO_DATA_SOURCES_SELECTED ]); - + this.ApplyFilters([], this.AllowsAutomaticSelection ? [ Event.RAG_AUTO_DATA_SOURCES_SELECTED ] : []); + // // Load the settings: // - this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection; - this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation; + this.aiBasedSourceSelection = this.IsAutomaticSelectionOn; + this.aiBasedValidation = this.IsAutomaticValidationOn; this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources; this.retrievalMode = this.DataSourceOptions.RetrievalMode; this.waitingForDataSources = this.areDataSourcesEnabled && this.SelectionMode is not DataSourceSelectionMode.CONFIGURATION_MODE; @@ -124,8 +160,8 @@ public partial class DataSourceSelection : MSGComponentBase { if (!this.internalChange) { - this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection; - this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation; + this.aiBasedSourceSelection = this.IsAutomaticSelectionOn; + this.aiBasedValidation = this.IsAutomaticValidationOn; this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources; this.retrievalMode = this.DataSourceOptions.RetrievalMode; this.selectedDataSources = this.GetDataSourcesFromConfiguredIds(); @@ -177,6 +213,21 @@ public partial class DataSourceSelection : MSGComponentBase this.StateHasChanged(); } + /// + /// Whether an AI selects the data sources, see AllowsAutomaticSelection. + /// + private bool IsAutomaticSelectionOn => this.AllowsAutomaticSelection && this.DataSourceOptions.AutomaticDataSourceSelection; + + /// + /// Whether an AI validates what the data sources gave, see AllowsAutomaticSelection. + /// + private bool IsAutomaticValidationOn => this.AllowsAutomaticSelection && this.DataSourceOptions.AutomaticValidation; + + /// + /// The models which chat with these data sources. + /// + private IReadOnlyList ChattingProviders => [this.LLMProvider, .. this.AdditionalProviders]; + private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection; private IReadOnlyCollection GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList(); @@ -189,9 +240,9 @@ public partial class DataSourceSelection : MSGComponentBase { this.DataSourceOptions = options; this.DataSourcesAISelected = aiSelectedDataSources ?? []; - - this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection; - this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation; + + this.aiBasedSourceSelection = this.IsAutomaticSelectionOn; + this.aiBasedValidation = this.IsAutomaticValidationOn; this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources; this.retrievalMode = this.DataSourceOptions.RetrievalMode; this.selectedDataSources = this.GetDataSourcesFromConfiguredIds(); @@ -263,14 +314,24 @@ public partial class DataSourceSelection : MSGComponentBase // would never come back, while the RAG process keeps reading it from the preselection. // // How the chat searches decides whether the agents of the RAG process see the data, and - // with that which data sources the provider of the chat may use at all. This component - // only ever selects for a chat: + // with that which data sources the provider of the chat may use at all. With several + // models, all of them search the same way, and each must be allowed every data source: // - var effectiveMode = await this.ToolRegistry.GetEffectiveRetrievalModeAsync(this.DataSourceOptions, this.LLMProvider, Tools.Components.CHAT); - var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.DataSourceOptions, effectiveMode.Mode, this.GetDataSourcesFromConfiguredIds()); + var preselectedDataSources = this.GetDataSourcesFromConfiguredIds(); + List modes = []; + foreach (var provider in this.ChattingProviders) + modes.Add(await this.ToolRegistry.GetEffectiveRetrievalModeAsync(this.DataSourceOptions, provider, this.Component)); + + var effectiveMode = SharedSelectionRules.RetrievalModeOfAll(modes); + List results = []; + foreach (var provider in this.ChattingProviders.Concat(this.AdditionalReaders)) + results.Add(await this.DataSourceService.GetDataSources(provider, this.DataSourceOptions, effectiveMode.Mode, preselectedDataSources)); + if (generation != this.loadAndApplyFiltersGeneration) return; + var sources = SharedSelectionRules.DataSourcesOfAll(results); + this.effectiveRetrievalMode = effectiveMode; this.availableDataSources = sources.AllowedDataSources; this.dataSourcesAwaitingReindex = sources.DataSourcesAwaitingReindex; @@ -392,6 +453,8 @@ public partial class DataSourceSelection : MSGComponentBase private string GetSemanticSearchUnavailableMessage() => this.effectiveRetrievalMode.SemanticSearchBlockReason switch { ToolOfferBlockReason.TOOLS_SWITCHED_OFF => TA("Your organization has switched tools off, so AI Studio uses classic RAG instead and searches your data sources with every message."), + ToolOfferBlockReason.MODEL_CANNOT_USE_TOOLS when this.AdditionalProviders.Count > 0 => TA("One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message."), + ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW when this.AdditionalProviders.Count > 0 => TA("One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message."), ToolOfferBlockReason.MODEL_CANNOT_USE_TOOLS => TA("The selected model cannot use tools, so AI Studio uses classic RAG instead and searches your data sources with every message."), ToolOfferBlockReason.TOOL_SWITCHED_OFF => TA("Your organization has switched Semantic Search off, so AI Studio uses classic RAG instead and searches your data sources with every message."), ToolOfferBlockReason.TOOL_SWITCHED_OFF_BY_USER => TA("You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message."), diff --git a/app/MindWork AI Studio/Components/DebouncedTextField.razor b/app/MindWork AI Studio/Components/DebouncedTextField.razor index 914ed34f4..944bcf71a 100644 --- a/app/MindWork AI Studio/Components/DebouncedTextField.razor +++ b/app/MindWork AI Studio/Components/DebouncedTextField.razor @@ -1,3 +1,4 @@ @inherits MudComponentBase - \ No newline at end of file +@* Bound by its value, the field checks itself only once touched. Bound by its text, it would check its first text, too, and show an issue as soon as it appears, e.g., when a switch shows it later: *@ + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/DebouncedTextField.razor.cs b/app/MindWork AI Studio/Components/DebouncedTextField.razor.cs index ddea74f13..be5da73ca 100644 --- a/app/MindWork AI Studio/Components/DebouncedTextField.razor.cs +++ b/app/MindWork AI Studio/Components/DebouncedTextField.razor.cs @@ -64,12 +64,22 @@ public partial class DebouncedTextField : MudComponentBase, IDisposable this.debounceTimer.Elapsed += (_, _) => { this.debounceTimer.Stop(); - this.InvokeAsync(async () => await this.TextChanged.InvokeAsync(this.text)).Observe($"{nameof(DebouncedTextField)}: notifying about changed text"); + this.InvokeAsync(async () => + { + // The parent hands this text back with its next render, which is no change of its own, cf. OnParametersSetAsync: + this.lastParameterText = this.text; + await this.TextChanged.InvokeAsync(this.text); + }).Observe($"{nameof(DebouncedTextField)}: notifying about changed text"); this.InvokeAsync(async () => await this.WhenTextChangedAsync(this.text)).Observe($"{nameof(DebouncedTextField)}: handling changed text asynchronously"); this.InvokeAsync(() => this.WhenTextCanged(this.text)).Observe($"{nameof(DebouncedTextField)}: handling changed text"); }; this.isInitialized = true; + + // A text the field starts with, e.g., one sent from another assistant, is handled like a typed one: + if (!string.IsNullOrWhiteSpace(this.text)) + this.debounceTimer.Start(); + await base.OnInitializedAsync(); } @@ -82,16 +92,19 @@ public partial class DebouncedTextField : MudComponentBase, IDisposable if(Math.Abs(this.debounceTimer.Interval - this.DebounceTime.TotalMilliseconds) > 1) this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds; - // Only sync when the parent's parameter actually changed since the last change: + // + // Only a text which the parent changed itself, e.g., by a reset, is synced and handled like a typed one. Every + // other render of the parent leaves the timer alone: restarting it there let its notification render the parent + // again, which restarted the timer, and so on, as long as the assistant stayed open. + // if (this.Text != this.lastParameterText) { this.text = this.Text; this.lastParameterText = this.Text; + this.debounceTimer.Stop(); + this.debounceTimer.Start(); } - - this.debounceTimer.Stop(); - this.debounceTimer.Start(); - + await base.OnParametersSetAsync(); } diff --git a/app/MindWork AI Studio/Components/EmbeddedChat.razor b/app/MindWork AI Studio/Components/EmbeddedChat.razor new file mode 100644 index 000000000..cb29e8742 --- /dev/null +++ b/app/MindWork AI Studio/Components/EmbeddedChat.razor @@ -0,0 +1,67 @@ +@using AIStudio.Chat +@inherits MSGComponentBase + +@if (this.conversation is null) +{ + +} +else if (this.conversation.Thread is not { } thread) +{ + + @T("AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it.") + +} +else +{ + @if (!this.conversation.HasVisibleMessages && !this.IsRunning && !string.IsNullOrWhiteSpace(this.Introduction)) + { + @this.Introduction + } + + @foreach (var block in thread.Blocks) + { + @if (block is { HideFromUser: false, Content: not null }) + { + + } + } + + @if (!this.IsReadOnly) + { + @if (this.ProviderIssue is { } issue) + { + @issue + } + + @* Laid out like the input of the chat page: sending through the icon in the field, and the tokens as its helper text. *@ + + + + @if (this.AllowsAttachments) + { + + } + @if (this.IsRunning) + { + @T("Stop") + } + + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/EmbeddedChat.razor.cs b/app/MindWork AI Studio/Components/EmbeddedChat.razor.cs new file mode 100644 index 000000000..d0a4dc319 --- /dev/null +++ b/app/MindWork AI Studio/Components/EmbeddedChat.razor.cs @@ -0,0 +1,336 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; +using AIStudio.Provider; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.Rust; +using AIStudio.Tools.Services; + +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace AIStudio.Components; + +/// +/// A chat which an assistant shows on its own page, such as one of the two chats of a task of the Model Comparison +/// assistant. +/// +/// +/// The chat itself lives in EmbeddedChatService, so an answer goes on while the user looks elsewhere; this component +/// shows it and renders whenever the service says it changed. What a request carries beside the conversation, such as +/// the system prompt and the tools, the assistant decides through PrepareAsync. The count of tokens leaves out the +/// context window of the model on purpose: it would tell models apart which the user compares without knowing them. +/// +public partial class EmbeddedChat : MSGComponentBase +{ + /// + /// Gets or sets where the chat lives. + /// + [Parameter, EditorRequired] + public IEmbeddedChatStore Store { get; set; } = null!; + + /// + /// Gets or sets the assistant which shows the chat. + /// + [Parameter, EditorRequired] + public Tools.Components Component { get; set; } + + /// + /// Gets or sets the ID of the chat. + /// + [Parameter, EditorRequired] + public Guid ChatId { get; set; } + + /// + /// Gets or sets how a chat which has no stored thread yet starts. + /// + [Parameter, EditorRequired] + public Func CreateThread { get; set; } = () => new(); + + /// + /// Gets or sets the provider which answers. + /// + [Parameter] + public AIStudio.Settings.Provider Provider { get; set; } = AIStudio.Settings.Provider.NONE; + + /// + /// Gets or sets how a request is readied before the message goes in, see EmbeddedChatRequest.PrepareAsync. + /// + [Parameter] + public Func> PrepareAsync { get; set; } = _ => Task.FromResult(true); + + /// + /// Gets or sets whether messages to the user leave out which provider and which model answer. + /// + [Parameter] + public bool HidesModelIdentity { get; set; } + + /// + /// Gets or sets whether the chat only shows its messages, without a field to write another one. + /// + [Parameter] + public bool IsReadOnly { get; set; } + + /// + /// Gets or sets whether the user may attach files to a message. + /// + [Parameter] + public bool AllowsAttachments { get; set; } = true; + + /// + /// Gets or sets which files the user may attach, or null for every file a chat takes. + /// + [Parameter] + public FileTypeFilter[]? AllowedFileTypes { get; set; } + + /// + /// Gets or sets a message which the chat sends by itself when it shows a conversation without any message of the + /// user, e.g., the first message of another chat on the same task. Sent once per chat, when the chat loads or, when + /// the message arrives later, as soon as it does; when it cannot be sent, it waits in the field, so that the user + /// can send it. See EmbeddedChatOpening.SendsNow. + /// + /// + /// The chat stores the message as it is, so give it a copy of its own, see ContentText.DeepClone. + /// + [Parameter] + public ContentText? OpeningMessage { get; set; } + + /// + /// Gets or sets why the provider cannot answer, or null when it can. + /// + [Parameter] + public string? ProviderIssue { get; set; } + + /// + /// Gets or sets what the chat says before its conversation begins, or nothing. + /// + [Parameter] + public string? Introduction { get; set; } + + /// + /// Gets or sets the name a message gets when the user exports it. + /// + [Parameter] + public string ExportFileName { get; set; } = string.Empty; + + [Inject] + private EmbeddedChatService ChatService { get; init; } = null!; + + [Inject] + private ConversationTokenCounter TokenCounter { get; init; } = null!; + + /// + /// Ends a count when the component goes away. + /// + private readonly CancellationTokenSource countCancellation = new(); + + /// + /// The token of countCancellation, taken while the source still lives, see BflChat. + /// + private CancellationToken countToken; + + /// + /// What the next request carries, as far as it was counted. + /// + private ConversationTokens tokens = ConversationTokens.UNAVAILABLE; + + /// + /// The provider the tokens were counted for, to count again when the parent chooses another one. + /// + private string countedProviderId = string.Empty; + + /// + /// The chat shown, or null while it loads. + /// + private EmbeddedConversation? conversation; + + /// + /// The ID of the chat shown, to notice when the parent shows another one. + /// + private Guid shownChatId; + + /// + /// The ID of the chat which sent its opening message already, so that it sends the message only once. + /// + private Guid openedChatId; + + /// + /// The message the user writes. + /// + private string userInput = string.Empty; + + /// + /// The files attached to the message the user writes. + /// + private HashSet attachments = []; + + /// + /// The attributes of the message field, such as the spell checking the user chose. + /// + private readonly Dictionary inputAttributes = new(); + + private bool IsRunning => this.conversation?.IsRunning ?? false; + + private bool CanSend => this.conversation is { IsAvailable: true, IsRunning: false } && this.ProviderIssue is null; + + private bool CanSendMessage => this.CanSend && !string.IsNullOrWhiteSpace(this.userInput); + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + this.countToken = this.countCancellation.Token; + this.ChatService.ConversationChanged += this.OnConversationChangedAsync; + this.SettingsManager.InjectSpellchecking(this.inputAttributes); + await base.OnInitializedAsync(); + } + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + if (this.shownChatId != this.ChatId) + { + this.shownChatId = this.ChatId; + this.conversation = null; + this.userInput = string.Empty; + this.attachments = []; + this.conversation = await this.ChatService.GetAsync(this.Store, this.Component, this.ChatId, this.CreateThread); + await this.RecountAsync(); + } + else if (this.countedProviderId != this.Provider.Id) + await this.RecountAsync(); + + // Not awaited: the answer arrives through ConversationChanged, and the parameters of the next render must not wait for it. + if (EmbeddedChatOpening.SendsNow(this.OpeningMessage, this.conversation, this.openedChatId == this.ChatId)) + { + this.openedChatId = this.ChatId; + _ = this.SendOpeningMessageAsync(this.OpeningMessage!); + } + } + + protected override void DisposeResources() + { + this.ChatService.ConversationChanged -= this.OnConversationChangedAsync; + this.countCancellation.Cancel(); + this.countCancellation.Dispose(); + base.DisposeResources(); + } + + #endregion + + /// + /// Renders the chat again when the service changed it, and counts again once an answer is complete. + /// + /// The ID of the chat which changed. + private Task OnConversationChangedAsync(Guid chatId) + { + if (chatId != this.ChatId) + return Task.CompletedTask; + + return this.InvokeAsync(async () => + { + if (!this.IsRunning) + await this.RecountAsync(); + + this.StateHasChanged(); + }); + } + + /// + /// Counts what the conversation carries so far, while no answer runs. + /// + /// + /// The system prompt counts as the chat stores it: what PrepareAsync adds for a request is not known before. The + /// count is therefore an estimate, and the message being written is left out. + /// + private async Task RecountAsync() + { + this.countedProviderId = this.Provider.Id; + if (this.IsReadOnly || this.conversation is not { Thread: { } thread, IsRunning: false } || this.ProviderIssue is not null || this.Provider.UsedLLMProvider is LLMProviders.NONE) + { + this.tokens = ConversationTokens.UNAVAILABLE; + return; + } + + try + { + var parts = ConversationParts.Of(thread, thread.SystemPrompt, string.Empty, null, false, null); + this.tokens = await this.TokenCounter.CountAsync(this.Provider, parts, thread.ReportedHistoryFor(this.Provider.Model), this.countToken); + } + catch (OperationCanceledException) + { + // The component went away. + } + } + + /// + /// Gets about how many tokens the conversation carries, or null when nothing could be counted. + /// + private string? TokenText => this.tokens.IsKnown + ? string.Format(T("approx. {0} tokens"), TokenAmount.Format(this.tokens.Tokens, Tools.PluginSystem.I18N.I.Culture)) + : null; + + private Task AttachmentsChanged(HashSet changedAttachments) + { + this.attachments = changedAttachments; + return Task.CompletedTask; + } + + /// + /// Sends the message of the user, with its attachments. + /// + private async Task SendAsync() + { + if (!this.CanSendMessage) + return; + + var message = new ContentText { Text = this.userInput.Trim(), FileAttachments = [.. this.attachments] }; + this.userInput = string.Empty; + this.attachments = []; + await this.ChatService.SendAsync(this.ChatId, this.RequestOf(message)); + } + + /// + /// Sends the opening message. When it cannot be sent, e.g., because the provider may not answer, the message waits + /// in the field instead, so that the user can send it once the reason is gone. + /// + /// The opening message. + private async Task SendOpeningMessageAsync(ContentText message) + { + if (this.CanSend && await this.ChatService.SendAsync(this.ChatId, this.RequestOf(message))) + return; + + await this.InvokeAsync(() => + { + this.userInput = message.Text; + this.attachments = [.. message.FileAttachments]; + this.StateHasChanged(); + }); + } + + /// + /// Gets the request which sends a message with the provider and the preparation of this chat. + /// + /// The message. + private EmbeddedChatRequest RequestOf(ContentText message) => new() + { + Provider = this.Provider, + Message = message, + HidesModelIdentity = this.HidesModelIdentity, + PrepareAsync = this.PrepareAsync, + }; + + /// + /// Sends the message when the user releases the shortcut they chose for the chat. + /// + /// The key released. + private async Task InputKeyEventAsync(KeyboardEventArgs keyEvent) + { + if (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior.IsSendShortcut(keyEvent)) + await this.SendAsync(); + } + + /// + /// Stops the answer the model writes. + /// + private Task CancelAsync() => this.ChatService.CancelAsync(this.ChatId); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor b/app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor new file mode 100644 index 000000000..66a373c3a --- /dev/null +++ b/app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor @@ -0,0 +1,13 @@ +@inherits MSGComponentBase + + + @* ProviderSelection marks its select as flex-grow-0, and that utility is declared !important, so StretchItems + cannot widen it. The width has to come from here. *@ +
+ +
+ @if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence) + { + + } +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor.cs b/app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor.cs new file mode 100644 index 000000000..d40ea3dca --- /dev/null +++ b/app/MindWork AI Studio/Components/ProviderSelectionWithConfidence.razor.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +/// +/// A provider selection with the confidence of the selected provider next to it, for a page which lets the user choose +/// several providers: the confidence card in the footer of an assistant knows only one of them. +/// +/// +/// The confidence shows only when the user wants to see it, see DataConfidence.ShowProviderConfidence. Like +/// ProviderSelection, this takes the component from the cascading parameter of its page. +/// +public partial class ProviderSelectionWithConfidence : MSGComponentBase +{ + /// + /// Gets or sets the selected provider. + /// + [Parameter] + public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE; + + /// + /// Gets or sets what happens when the user selects another provider. + /// + [Parameter] + public EventCallback ProviderSettingsChanged { get; set; } + + /// + /// Gets or sets the validation of the selected provider, which returns an issue or null. + /// + [Parameter] + public Func ValidateProvider { get; set; } = _ => null; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor.cs b/app/MindWork AI Studio/Components/ToolSelection.razor.cs index 95dbfe07d..dd1cf7e28 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ToolSelection.razor.cs @@ -15,6 +15,17 @@ public partial class ToolSelection : MSGComponentBase [Parameter] public required AIStudio.Settings.Provider LLMProvider { get; set; } + /// + /// Further models which work with the same tools, e.g., the second model of a comparison series. + /// + /// + /// The selection then offers only what every model may use: tools only when each of them can call tools, and each + /// tool by the lowest confidence among them, see SharedSelectionRules. Without further models, it offers what + /// LLMProvider alone may use. + /// + [Parameter] + public IReadOnlyList AdditionalProviders { get; set; } = []; + /// /// Where the chat of this selection may still send data, because of the mailboxes it read from. /// @@ -60,7 +71,12 @@ public partial class ToolSelection : MSGComponentBase await base.OnInitializedAsync(); } - private ToolCallingAvailability ToolCallingAvailability => this.LLMProvider.GetToolCallingAvailability(); + /// + /// Every model which works with the selected tools. + /// + private IEnumerable AllProviders => [this.LLMProvider, .. this.AdditionalProviders]; + + private ToolCallingAvailability ToolCallingAvailability => SharedSelectionRules.ToolCallingOfAll(this.AllProviders.Select(provider => provider.GetToolCallingAvailability())); private bool SupportsTools => this.ToolCallingAvailability.IsAvailable; @@ -70,9 +86,12 @@ public partial class ToolSelection : MSGComponentBase private string UnsupportedToolsMessage => this.ToolCallingAvailability.Message; - private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE + /// + /// The confidence which counts for the tools: the lowest of all models, see SharedSelectionRules.LowestConfidence. + /// + private ConfidenceLevel ProviderConfidence => SharedSelectionRules.LowestConfidence(this.AllProviders.Select(provider => provider == AIStudio.Settings.Provider.NONE ? ConfidenceLevel.NONE - : this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level; + : provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level)); private async Task ToggleSelection() { @@ -193,7 +212,9 @@ public partial class ToolSelection : MSGComponentBase return null; return string.Format( - this.T("This tool requires provider confidence {0}. The selected provider has {1}."), + this.AdditionalProviders.Count == 0 + ? this.T("This tool requires provider confidence {0}. The selected provider has {1}.") + : this.T("This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}."), GetMinimumProviderConfidence(item).Name, this.ProviderConfidence.Name); } diff --git a/app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor b/app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor new file mode 100644 index 000000000..bd6193f75 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor @@ -0,0 +1,165 @@ +@using AIStudio.Assistants.ModelComparison +@inherits MSGComponentBase + + + + @ModelComparisonResultTexts.WhatTheResultsShow + + + + + + + + + @if (this.statistics is null) + { + + } + else if (this.statistics.IsEmpty) + { + + @TA("There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”.") + + } + else + { + var shownStatistics = this.statistics; + @if (shownStatistics.ComparisonCount > 0) + { + + @string.Format(ModelComparisonFormat.Culture, T("{0}: {1} comparisons with {2} runs."), ModelComparisonMode.STABILITY.Name, shownStatistics.ComparisonCount.CompactCount(), shownStatistics.RunCount.CompactCount()) + + } + @if (shownStatistics.SeriesCount > 0) + { + + @string.Format(ModelComparisonFormat.Culture, T("{0}: {1} comparison series with {2} tasks decided without knowing the models."), ModelComparisonMode.TASKS.Name, shownStatistics.SeriesCount.CompactCount(), shownStatistics.TaskCount.CompactCount()) + + } + + + + + @TA("How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers.") + + + @ModelComparisonResultTexts.HowToReadTheShares + + @if (shownStatistics.VoteRanking.Count == 0) + { + + @TA("You have not voted on two different models yet.") + + } + else + { + + } + + + + @if (shownStatistics.Pairings.Count == 0) + { + + @T("No two different models were compared with each other yet.") + + } + else + { + + @TA("How often each of two models won when they competed directly, by your votes and by the verdicts of the judges.") + + + @ModelComparisonResultTexts.WhatThePairAssessmentMeans + + @foreach (var pairing in shownStatistics.Pairings) + { + + } + } + + + + @if (shownStatistics.JudgeAgreements.Count == 0 && shownStatistics.JudgeRanking.Count == 0) + { + + @T("No judge has given a usable verdict yet.") + + } + else + { + @if (shownStatistics.JudgeAgreements.Count > 0) + { + + @TA("Agreement with your votes") + + + @TA("How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count.") + + + @ModelComparisonResultTexts.WhatTheAgreementMeans + + + + + @T("Judge") + @T("Compared") + @T("Same choice") + @T("Agreement") + + + + @foreach (var agreement in shownStatistics.JudgeAgreements) + { + + @agreement.Judge.Label + @agreement.ComparedRuns.CompactCount() + @agreement.Agreements.CompactCount() + @ModelComparisonFormat.Share(agreement.Share) + + } + + + } + + @if (shownStatistics.JudgeRanking.Count > 0) + { + + @T("Ranking by the judges") + + + @T("How often each model won by the verdicts of the judges.") + + + } + } + + + + @if (shownStatistics.Speeds.Count == 0) + { + + @T("Only the mode “Answer Variance” measures how fast the models answer.") + + } + else + { + + } + + + } + + + @if (this.DeletesResults || this.DeletesSeries) + { + + @this.DeleteText + + } + + @T("Close") + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor.cs new file mode 100644 index 000000000..db4db114f --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ModelComparisonResultsDialog.razor.cs @@ -0,0 +1,174 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Chat.Embedded; +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +/// +/// Shows what all stored comparisons of the Model Comparison assistant say about the models: which model wins, which +/// wins against which, how often the judges agree with the user, and how fast each model answers. +/// +/// +/// The charts and their tables are components of the assistant, see ModelComparisonRanking, ModelComparisonPairCard, +/// and ModelComparisonSpeeds; ModelComparisonCharts explains their colors. +/// +public partial class ModelComparisonResultsDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Inject] + private ModelComparisonStore ModelComparisonStore { get; init; } = null!; + + [Inject] + private EmbeddedChatService ChatService { get; init; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + /// + /// The statistics of all stored comparisons, or null while they are being read. + /// + private ModelComparisonStatistics? statistics; + + /// + /// All comparison series, also those whose result the user has not seen yet; none while the source counts the + /// compared answers alone. + /// + private IReadOnlyList listedSeries = []; + + /// + /// Which results the statistics count. + /// + private ModelComparisonResultSource source = ModelComparisonResultSource.ALL; + + private bool isDeleting; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await this.LoadStatisticsAsync(); + } + + #endregion + + /// + /// Reads the results the source names anew. The comparison series are read for every source but the compared + /// answers alone: they are small, since their chats stay on the disk. + /// + private async Task LoadStatisticsAsync() + { + this.statistics = null; + this.listedSeries = this.source is ModelComparisonResultSource.ANSWERS ? [] : await this.ModelComparisonStore.ListSeriesAsync(); + this.statistics = await ModelComparisonStatistics.ComputeAsync(this.ModelComparisonStore.ReadResultsAsync(), this.listedSeries, this.source); + } + + private async Task SourceChanged(ModelComparisonResultSource changedSource) + { + this.source = changedSource; + await this.LoadStatisticsAsync(); + } + + /// + /// Whether deleting takes the results of the mode Answer Variance along: the chosen source counts them, and there + /// are some. + /// + private bool DeletesResults => this.source is not ModelComparisonResultSource.TASKS && this.statistics is { ComparisonCount: > 0 }; + + /// + /// Whether deleting takes the comparison series along: the chosen source counts them, and there are some. The + /// results of the mode Everyday Tasks are the decided tasks of the series, so deleting them means deleting the + /// series, also those whose result the user has not seen yet, for a fresh start. + /// + private bool DeletesSeries => this.source is not ModelComparisonResultSource.ANSWERS && this.listedSeries.Count > 0; + + private string DeleteText => this.DeletesResults + ? this.DeletesSeries ? T("Delete all results and comparison series") : T("Delete the results of “Answer Variance”") + : T("Delete all comparison series"); + + private string DeleteQuestion => this.DeletesResults + ? this.DeletesSeries + ? TA("Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone.") + : TA("Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone.") + : TA("Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone."); + + /// + /// Deletes what the chosen source counts, after asking. + /// + private async Task DeleteAsync() + { + var deletesResults = this.DeletesResults; + var deletesSeries = this.DeletesSeries; + if (!deletesResults && !deletesSeries) + return; + + var parameters = new DialogParameters + { + { dialog => dialog.Message, this.DeleteQuestion }, + }; + + var reference = await this.DialogService.ShowAsync(this.DeleteText, parameters, DialogOptions.FULLSCREEN); + var result = await reference.Result; + if (result is null || result.Canceled) + return; + + this.isDeleting = true; + try + { + if (deletesResults) + await this.DeleteResultsAsync(); + + if (deletesSeries) + await this.DeleteSeriesAsync(); + } + finally + { + this.isDeleting = false; + } + + // Reads anew in any case: a failure may have deleted a part already. + await this.LoadStatisticsAsync(); + } + + private async Task DeleteResultsAsync() + { + try + { + await this.ModelComparisonStore.DeleteAllResultsAsync(); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not delete the results of the Model Comparison assistant."); + await this.MessageBus.SendError(new(AppIcons.ASSISTANT_MODEL_COMPARISON, TA("AI Studio could not delete all results. Please try again."))); + } + } + + private async Task DeleteSeriesAsync() + { + // An answer which still runs would otherwise try to store its chat into a deleted series: + foreach (var task in this.listedSeries.SelectMany(series => series.Tasks)) + { + await this.ChatService.ForgetAsync(task.ChatOne.ChatId); + await this.ChatService.ForgetAsync(task.ChatTwo.ChatId); + } + + try + { + await this.ModelComparisonStore.DeleteAllSeriesAsync(); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not delete the comparison series of the Model Comparison assistant."); + await this.MessageBus.SendError(new(AppIcons.ASSISTANT_MODEL_COMPARISON, TA("AI Studio could not delete all comparison series. Please try again."))); + } + } + + private void Close() => this.MudDialog.Close(DialogResult.Ok(true)); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor b/app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor new file mode 100644 index 000000000..a18d103bd --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor @@ -0,0 +1,96 @@ +@using AIStudio.Assistants.ModelComparison +@using AIStudio.Settings.DataModel +@inherits MSGComponentBase + + + + @* A dialog does not get the cascading values of the page which opened it, and the model selections need this one: *@ + + + @ModelComparisonSeriesTexts.WhatASeriesIs + + + @if (this.BasedOn is not null) + { + + @this.BasedOnText + @foreach (var part in this.missingParts) + { + @this.MissingPartText(part) + } + + } + + @T("Name") + + + @T("Models") + + @ModelComparisonSeriesTexts.WhatTheModelsAreFor + + + + @T("First model") + + + + @T("Second model") + + + + + @* The form checks the fields of the judge also while the panel is collapsed: *@ + + + @ModelComparisonSeriesTexts.WhatTheJudgeIsFor + + + @if (this.judgeEnabled) + { + @T("Judge model") + + + } + + + @T("Profile, tools, and data sources") + + @ModelComparisonSeriesTexts.WhatBothModelsShare + + @if (this.BothModelsChosen) + { + + + + @if (this.SettingsManager.AreToolsEnabled()) + { + + } + + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) + { + + } + + @this.SelectionSummary + + } + else + { + + @TA("Choose both models first: what you can select here depends on them.") + + } + + + + + + + @T("Cancel") + + + @T("Create series") + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor.cs new file mode 100644 index 000000000..61bc2e311 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ModelComparisonSeriesDialog.razor.cs @@ -0,0 +1,382 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Components; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.Services; +using AIStudio.Tools.ToolCallingSystem; + +using Microsoft.AspNetCore.Components; + +using MudBlazor.Utilities; + +namespace AIStudio.Dialogs; + +/// +/// The dialog with which the user begins a comparison series of the Model Comparison assistant. It stores the series +/// and closes with it. +/// +/// +/// A fair comparison needs the same setup for both models. The selections therefore offer only the tools which both +/// models may use, and only the data sources which both models and the judge may see: the judge reads both chats, and +/// with them what the data sources gave. Both chats also search the data sources the same way, by Semantic Search only +/// when both models can use it, see SharedSelectionRules. What the series stores is checked once more the same way, +/// since a selection may still hold what a model changed since keeps from it. +/// +public partial class ModelComparisonSeriesDialog : MSGComponentBase +{ + private const Tools.Components COMPONENT = Tools.Components.MODEL_COMPARISON_ASSISTANT; + + /// + /// How long the name of a series may be, see ModelComparisonNames. + /// + private const int MAX_NAME_LENGTH = ModelComparisonNames.MAX_SERIES_NAME_LENGTH; + + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The series whose setup the new one takes over, or null for a new series with the preselected options. + /// + /// + /// Only the setup carries over; the tasks stay in their series. What is no longer available is left out, see + /// ModelComparisonSeriesCopy. + /// + [Parameter] + public ModelComparisonSeries? BasedOn { get; set; } + + [Inject] + private ToolRegistry ToolRegistry { get; init; } = null!; + + [Inject] + private DataSourceService DataSourceService { get; init; } = null!; + + [Inject] + private ModelComparisonStore ModelComparisonStore { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + private string seriesName = string.Empty; + + /// + /// The names of the existing series, which a new series must not take again. + /// + private IReadOnlyList takenSeriesNames = []; + + private AIStudio.Settings.Provider firstProvider = AIStudio.Settings.Provider.NONE; + private AIStudio.Settings.Provider secondProvider = AIStudio.Settings.Provider.NONE; + private bool judgeEnabled; + private bool judgePanelExpanded; + private AIStudio.Settings.Provider judgeProvider = AIStudio.Settings.Provider.NONE; + + /// + /// The expectations, values, and goals of the user, by which the judge decides, see ModelComparisonJudgeExpectations. + /// + private string judgeInstructions = string.Empty; + + /// + /// Whether the user confirmed that the judge may decide by its own standards, which counts only while + /// judgeInstructions is empty. + /// + private bool judgeOwnStandardsAccepted; + + private Profile profile = Profile.NO_PROFILE; + private HashSet selectedToolIds = []; + + /// + /// The data source options both chats get. An AI never selects or validates the data sources of a series, since it + /// could treat the two models differently, see ModelComparisonSeriesThreads. + /// + private DataSourceOptions dataSourceOptions = new() + { + DisableDataSources = true, + AutomaticDataSourceSelection = false, + AutomaticValidation = false, + RetrievalMode = DataSourceRetrievalMode.SEMANTIC_SEARCH, + }; + + /// + /// The parts of the series in BasedOn which the new series cannot take over. + /// + private IReadOnlyList missingParts = []; + + private bool dataIsValid; + private string[] dataIssues = []; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + /// + /// Whether the user has tried to create the series at least once. The field of the judge model mounts only once + /// the judge is switched on, and it stays silent until then, see ValidatingJudgeModel. Afterward, every change + /// checks the form anew, see RevalidateAfterFieldChange. + /// + private bool hasAttemptedCreate; + + /// + /// Whether the form has to be checked anew once the fields hold their new values, i.e., after the next render. + /// + private bool revalidatesAfterRender; + + private bool isSaving; + + #region Overrides of MSGComponentBase + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + this.takenSeriesNames = [.. (await this.ModelComparisonStore.ListSeriesAsync()).Select(existingSeries => existingSeries.Name)]; + if (this.BasedOn is { } series) + { + var selectableToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(COMPONENT, series.ToolIds); + var copy = ModelComparisonSeriesCopy.Of(this.SettingsManager, series, selectableToolIds); + + // The name of the series itself is taken, so the new one counts on, e.g., "Weekly reports (2)": + this.seriesName = ModelComparisonNames.FreeNameFrom(series.Name, this.takenSeriesNames, MAX_NAME_LENGTH); + this.firstProvider = copy.First; + this.secondProvider = copy.Second; + this.judgeEnabled = copy.JudgeEnabled; + this.judgePanelExpanded = copy.JudgeEnabled; + this.judgeProvider = copy.Judge; + this.judgeInstructions = series.JudgeInstructions; + this.profile = copy.Profile; + this.selectedToolIds = copy.ToolIds; + this.dataSourceOptions = copy.DataSourceOptions; + this.missingParts = copy.MissingParts; + return; + } + + this.profile = this.SettingsManager.GetPreselectedProfile(COMPONENT); + this.firstProvider = this.SettingsManager.PreselectedComparisonModel; + this.judgeProvider = this.SettingsManager.PreselectedComparisonJudge; + this.judgeEnabled = this.judgeProvider != AIStudio.Settings.Provider.NONE; + this.judgePanelExpanded = this.judgeEnabled; + } + + #endregion + + #region Overrides of ComponentBase + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // We don't want to show validation errors when the user opens the dialog: + if (firstRender) + await this.form.ResetValidationAsync(); + + // A check asked for after a change waits until the fields hold their new values, cf. DependentFieldValidation: + if (this.revalidatesAfterRender) + { + this.revalidatesAfterRender = false; + await this.form.ValidateAsync(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + private bool BothModelsChosen => this.firstProvider.UsedLLMProvider is not LLMProviders.NONE && this.secondProvider.UsedLLMProvider is not LLMProviders.NONE; + + /// + /// The second model, as the selections take further models. + /// + private IReadOnlyList SecondModel => [this.secondProvider]; + + /// + /// The models which read what the data sources gave without searching them: the judge, while it is switched on. + /// + private IReadOnlyList Readers => this.judgeEnabled && this.judgeProvider.UsedLLMProvider is not LLMProviders.NONE + ? [this.judgeProvider] + : []; + + /// + /// Says in short what is selected, next to the buttons which hide the selections in their popovers. + /// + private string SelectionSummary => string.Format( + T("Tools: {0}, data sources: {1}"), + this.selectedToolIds.Count.CompactCount(), + (this.dataSourceOptions.DisableDataSources ? 0 : this.dataSourceOptions.PreselectedDataSourceIds.Count).CompactCount()); + + /// + /// Tells what the new series starts with, when it takes over the setup of another one. + /// + private string BasedOnText => string.Format(TA("This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series."), this.BasedOn?.Name); + + /// + /// Tells why a part of the series in BasedOn is left out, and what the user can do about it. + /// + private string MissingPartText(ModelComparisonSeriesPart part) => part switch + { + ModelComparisonSeriesPart.FIRST_MODEL => TA("The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew."), + ModelComparisonSeriesPart.SECOND_MODEL => TA("The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew."), + ModelComparisonSeriesPart.JUDGE => TA("The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off."), + ModelComparisonSeriesPart.PROFILE => TA("The profile no longer exists, so the series starts without a profile."), + ModelComparisonSeriesPart.TOOLS => TA("Some tools are no longer available, so the series starts without them."), + ModelComparisonSeriesPart.DATA_SOURCES => TA("Some data sources are no longer available, so the series starts without them."), + _ => string.Empty, + }; + + /// + /// Checks the name of the series, see ModelComparisonNames.SeriesNameIssue. + /// + /// The issue, or null when the name is fine. + private string? ValidatingSeriesName(string name) => ModelComparisonNames.SeriesNameIssue(name, this.takenSeriesNames); + + private string? ValidatingFirstModel(AIStudio.Settings.Provider provider) => ModelComparisonProviderCheck.IssueOf(this.SettingsManager, provider); + + /// + /// Checks the second model, which must be another model than the first one: a series compares how well two models + /// help the user. + /// + /// The issue, or null when the model is fine. + private string? ValidatingSecondModel(AIStudio.Settings.Provider provider) + { + if (ModelComparisonProviderCheck.IssueOf(this.SettingsManager, provider) is { } issue) + return issue; + + return ModelComparisonModelIdentity.Of(this.firstProvider).IsSameModelAs(ModelComparisonModelIdentity.Of(provider)) + ? TA("Please choose two different models. A series compares how well two models help you.") + : null; + } + + /// + /// Checks the judge model. + /// + /// + /// Silent until hasAttemptedCreate: this field mounts only once the judge is switched on, often well after the form + /// first rendered, and MudForm would otherwise flag it as soon as it exists, before the user had any chance to fill + /// it in. The same holds for the judge model of the mode Answer Variance. + /// + private string? ValidatingJudgeModel(AIStudio.Settings.Provider provider) => this.hasAttemptedCreate ? ModelComparisonProviderCheck.IssueOf(this.SettingsManager, provider) : null; + + /// + /// Whether one of the fields of the switched-on judge has an issue: its model, or the expectations of the user + /// without the confirmation that the judge may decide by its own standards. + /// + private bool JudgeHasIssue => this.judgeEnabled && (ModelComparisonProviderCheck.IssueOf(this.SettingsManager, this.judgeProvider) is not null || ModelComparisonJudgeExpectations.IssueOf(this.judgeInstructions, this.judgeOwnStandardsAccepted) is not null); + + /// + /// The second model is checked against the first one, which a MudForm does not know: it checks a field only when + /// that very field changes. Once the user tried to create the series and saw the issues, every change checks the + /// whole form anew, so that an issue which no longer holds goes away. + /// + private void RevalidateAfterFieldChange(FormFieldChangedEventArgs _) + { + if (this.hasAttemptedCreate) + this.revalidatesAfterRender = true; + } + + private void FirstProviderChanged(AIStudio.Settings.Provider provider) => this.firstProvider = provider; + + private void SecondProviderChanged(AIStudio.Settings.Provider provider) => this.secondProvider = provider; + + private void JudgeEnabledChanged(bool enabled) => this.judgeEnabled = enabled; + + private void JudgeProviderChanged(AIStudio.Settings.Provider provider) => this.judgeProvider = provider; + + private void ProfileChanged(Profile changedProfile) => this.profile = changedProfile; + + private void ToolsChanged(HashSet toolIds) => this.selectedToolIds = toolIds; + + private void DataSourceOptionsChanged(DataSourceOptions options) => this.dataSourceOptions = options; + + /// + /// Gets the selected tools which both models may use. + /// + private List ToolsForBoth() + { + var toolIds = this.ToolRegistry.NormalizeSelection(this.selectedToolIds) + .Where(toolId => this.MayUseTool(this.firstProvider, toolId) && this.MayUseTool(this.secondProvider, toolId)); + + return [.. this.ToolRegistry.NormalizeSelection(toolIds)]; + } + + /// + /// Whether a model may use a tool. Which data a chat holds is decided per request; here, nothing restricts it yet. + /// + private bool MayUseTool(AIStudio.Settings.Provider provider, string toolId) => this.ToolRegistry.FilterToolIdsForProvider(provider, [toolId], OutboundDataRestriction.UNRESTRICTED).Count > 0; + + /// + /// Gets the selected data sources which every model that sees the data may see, and the way both chats search them. + /// + /// The data sources and how they are searched, or NONE without any data source. + private async Task DataSourcesForAllAsync() + { + if (this.dataSourceOptions.DisableDataSources || !PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) + return ModelComparisonSeriesDataSources.NONE; + + List modes = []; + foreach (var provider in new[] { this.firstProvider, this.secondProvider }) + modes.Add(await this.ToolRegistry.GetEffectiveRetrievalModeAsync(this.dataSourceOptions, provider, COMPONENT)); + + var retrievalMode = SharedSelectionRules.RetrievalModeOfAll(modes).Mode; + var preselectedDataSources = this.SettingsManager.ConfigurationData.DataSources + .Where(dataSource => this.dataSourceOptions.PreselectedDataSourceIds.Contains(dataSource.Id)) + .ToList(); + + List results = []; + foreach (var provider in new[] { this.firstProvider, this.secondProvider }.Concat(this.Readers)) + results.Add(await this.DataSourceService.GetDataSources(provider, this.dataSourceOptions, retrievalMode, preselectedDataSources)); + + List dataSourceIds = [.. SharedSelectionRules.DataSourcesOfAll(results).SelectedDataSources.Select(dataSource => dataSource.Id)]; + return dataSourceIds.Count > 0 + ? new(dataSourceIds, retrievalMode) + : ModelComparisonSeriesDataSources.NONE; + } + + private async Task CreateAsync() + { + // Set before validating, so the judge model -- silent about being empty until now -- reports its issue at once: + this.hasAttemptedCreate = true; + await this.form.ValidateAsync(); + + // + // The fields of the judge sit in a panel the user may have closed. The form checks them all the same, but the + // user would not see which one is wrong. So the dialog opens the panel: + // + if (this.JudgeHasIssue) + this.judgePanelExpanded = true; + + // When the data is not valid, we don't store it: + if (!this.dataIsValid) + return; + + this.isSaving = true; + try + { + var dataSources = await this.DataSourcesForAllAsync(); + var series = new ModelComparisonSeries + { + Id = Guid.NewGuid(), + Name = this.seriesName.Trim(), + CreatedAtUtc = DateTimeOffset.UtcNow, + FirstProviderId = this.firstProvider.Id, + SecondProviderId = this.secondProvider.Id, + First = ModelComparisonModelIdentity.Of(this.firstProvider), + Second = ModelComparisonModelIdentity.Of(this.secondProvider), + JudgeProviderId = this.judgeEnabled ? this.judgeProvider.Id : string.Empty, + Judge = this.judgeEnabled ? ModelComparisonModelIdentity.Of(this.judgeProvider) : null, + JudgeInstructions = this.judgeEnabled ? this.judgeInstructions.Trim() : string.Empty, + ProfileId = this.profile.Id, + ToolIds = this.ToolsForBoth(), + DataSourceIds = [.. dataSources.DataSourceIds], + RetrievalMode = dataSources.RetrievalMode, + }; + + await this.ModelComparisonStore.CreateSeriesAsync(series); + this.MudDialog.Close(DialogResult.Ok(series)); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogError(exception, "Could not store a new comparison series of the Model Comparison assistant."); + await this.MessageBus.SendError(new(AppIcons.ASSISTANT_MODEL_COMPARISON, TA("AI Studio could not save the new comparison series. Please try again."))); + } + finally + { + this.isSaving = false; + } + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor new file mode 100644 index 000000000..a521a52f3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor @@ -0,0 +1,29 @@ +@using AIStudio.Settings +@inherits SettingsDialogBase + + + + + + @T("Assistant: Model Comparison Options") + + + + + + + + + + @if (this.SelectedPresetMissing) + { + @TA("The preselected preset no longer exists. Please select another preset.") + } + + + + + @T("Close") + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor.cs new file mode 100644 index 000000000..d24761750 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogModelComparison.razor.cs @@ -0,0 +1,82 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs.Settings; + +public partial class SettingsDialogModelComparison : SettingsDialogBase +{ + [Inject] + private ModelComparisonStore ModelComparisonStore { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + /// + /// The presets of the mode Answer Variance, as the store listed them when the dialog opened. + /// + private IReadOnlyList presets = []; + + /// + /// Whether the presets were listed. Until then, a preselected preset does not count as missing. + /// + private bool presetsListed; + + private bool OptionsDisabled() => !this.SettingsManager.ConfigurationData.ModelComparison.PreselectOptions; + + #region Overrides of SettingsDialogBase + + /// + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + try + { + this.presets = await this.ModelComparisonStore.ListPresetsAsync(); + this.presetsListed = true; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + this.Logger.LogWarning(exception, "Could not list the presets of the Model Comparison assistant."); + } + } + + #endregion + + /// + /// Gets the entries of the preset selection: none, every preset, and the preselected one when it no longer exists. + /// + private IReadOnlyList> PresetData + { + get + { + var selectedPresetId = this.SettingsManager.ConfigurationData.ModelComparison.PreselectedPresetId; + List> entries = + [ + new(T("No preset preselected"), string.Empty), + .. this.presets.Select(preset => new ConfigurationSelectData(preset.Name, preset.Id.ToString("D"))) + ]; + + if (this.SelectedPresetMissing) + entries.Add(new(string.Format(T("Missing preset ({0})"), selectedPresetId), selectedPresetId)); + + return entries; + } + } + + /// + /// Gets whether the preselected preset was deleted since. + /// + private bool SelectedPresetMissing + { + get + { + var selectedPresetId = this.SettingsManager.ConfigurationData.ModelComparison.PreselectedPresetId; + return this.presetsListed + && !string.IsNullOrWhiteSpace(selectedPresetId) + && this.presets.All(preset => !string.Equals(preset.Id.ToString("D"), selectedPresetId, StringComparison.OrdinalIgnoreCase)); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index 2e178aab9..452cd2cb0 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -19,6 +19,7 @@ + diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 85d7c4001..fad58b034 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -920,8 +920,8 @@ CONFIG["SETTINGS"] = {} -- LEGAL_CHECK_ASSISTANT, SYNONYMS_ASSISTANT, MY_TASKS_ASSISTANT, -- JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, ERI_ASSISTANT, -- DOCUMENT_ANALYSIS_ASSISTANT, BATCH_PROCESSING_ASSISTANT, SLIDE_BUILDER_ASSISTANT, --- VISUAL_BRIEFING_ASSISTANT, BEYOND_FACTS_LAB_ASSISTANT, I18N_ASSISTANT, --- LOG_VIEWER_ASSISTANT +-- VISUAL_BRIEFING_ASSISTANT, BEYOND_FACTS_LAB_ASSISTANT, MODEL_COMPARISON_ASSISTANT, +-- I18N_ASSISTANT, LOG_VIEWER_ASSISTANT -- -- Replaces, does not merge: a configuration with a higher priority replaces this list -- completely. This is what lets a department show an assistant again that the base @@ -1010,6 +1010,22 @@ CONFIG["SETTINGS"] = {} -- users with the prompts which come with AI Studio. -- CONFIG["SETTINGS"]["DataBeyondFactsLab.AllowPromptOverrides"] = false +-- Configure defaults for the Model Comparison assistant. +-- Preselection must be enabled for the remaining Model Comparison settings to take effect. +-- CONFIG["SETTINGS"]["DataModelComparison.PreselectOptions"] = true +-- +-- Configure the minimum provider confidence which every compared model and every judge needs. +-- Allowed values are: NONE, UNTRUSTED, UNKNOWN, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH +-- CONFIG["SETTINGS"]["DataModelComparison.MinimumProviderConfidence"] = "NONE" +-- +-- Configure the model which new presets and comparison series start with as their first model, +-- and the judge which they start with. A preselected judge also switches the judge on. +-- Use one of the provider IDs defined in CONFIG["LLM_PROVIDERS"], an empty text for the +-- app-wide default provider, or "00000000-0000-0000-0000-000000000000" for no model at all, +-- which is also the default. +-- CONFIG["SETTINGS"]["DataModelComparison.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000" +-- CONFIG["SETTINGS"]["DataModelComparison.PreselectedJudgeProvider"] = "00000000-0000-0000-0000-000000000000" + -- Configure defaults for the Assistant Builder. -- Preselection must be enabled for the remaining Assistant Builder settings to take effect. -- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectOptions"] = true diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index f847da826..23d466900 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -4617,6 +4617,990 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133 -- Clear UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Löschen" +-- Material: {0} characters +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1097973848"] = "Material: {0} Zeichen" + +-- Judge (optional) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1206060603"] = "Richter (optional)" + +-- Give your preset a name which tells you later what it compares. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1326490469"] = "Geben Sie Ihrer Voreinstellung einen Namen, aus dem später hervorgeht, was sie vergleicht." + +-- Give your preset a name which tells you later what it compares. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1326490469::INFORMAL"] = "Gib deiner Voreinstellung einen Namen, aus dem später hervorgeht, was sie vergleicht." + +-- Please provide a name for your preset. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1408294373"] = "Bitte geben Sie einen Namen für Ihre Voreinstellung ein." + +-- Please provide a name for your preset. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1408294373::INFORMAL"] = "Bitte gib einen Namen für deine Voreinstellung ein." + +-- 20 (heavy load on the models) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1426001529"] = "20 (hohe Auslastung der Modelle)" + +-- Please provide a question or task for both models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1481551697"] = "Bitte geben Sie eine Frage oder Aufgabe für beide Modelle ein." + +-- Please provide a question or task for both models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1481551697::INFORMAL"] = "Bitte gib eine Frage oder Aufgabe für beide Modelle ein." + +-- Delete this preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T148310729"] = "Diese Voreinstellung löschen" + +-- Your request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1607440787"] = "Ihre Anfrage" + +-- Your request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1607440787::INFORMAL"] = "Deine Anfrage" + +-- Your question or task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1646098156"] = "Ihre Frage oder Aufgabe" + +-- Your question or task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1646098156::INFORMAL"] = "Deine Frage oder Aufgabe" + +-- Material (optional) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1771342343"] = "Material (optional)" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1990305431"] = "Ergebnisse aller Vergleiche" + +-- Switch mode +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2045889708"] = "Modus wechseln" + +-- Add preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2059304803"] = "Voreinstellung hinzufügen" + +-- 1 (single comparison) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2267053576"] = "1 (einzelner Vergleich)" + +-- No judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2562272449"] = "Kein Richter" + +-- AI Studio could not save the results of this comparison for the statistics. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2762349476"] = "AI Studio konnte die Ergebnisse dieses Vergleichs nicht für die Statistik speichern." + +-- Preset from {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2841002060"] = "Voreinstellung vom {0}" + +-- Models +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T30216107"] = "Modelle" + +-- The name of your preset must be between 6 and 60 characters long. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3065222761"] = "Der Name Ihrer Voreinstellung muss zwischen 6 und 60 Zeichen lang sein." + +-- The name of your preset must be between 6 and 60 characters long. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3065222761::INFORMAL"] = "Der Name deiner Voreinstellung muss zwischen 6 und 60 Zeichen lang sein." + +-- A judge assesses every run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3109664129"] = "Ein Richter beurteilt jeden Durchlauf" + +-- Are you sure you want to delete the preset '{0}'? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3246395362"] = "Möchten Sie die Voreinstellung „{0}“ wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Are you sure you want to delete the preset '{0}'? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3246395362::INFORMAL"] = "Möchtest du die Voreinstellung „{0}“ wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Delete preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3276319725"] = "Voreinstellung löschen" + +-- Material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3419754368"] = "Material" + +-- Compare +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3581545044"] = "Vergleichen" + +-- Ask a judge? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3641185871"] = "Einen Richter fragen?" + +-- Load material from a file +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3699307713"] = "Material aus einer Datei laden" + +-- Expand +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3889190145"] = "Ausklappen" + +-- Presets +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3897629751"] = "Voreinstellungen" + +-- Preset name +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T40334785"] = "Name der Voreinstellung" + +-- A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4043921725"] = "Eine Voreinstellung speichert alle Einstellungen, die Sie unten vornehmen. So können Sie denselben Vergleich später erneut durchführen, zum Beispiel wenn ein neues Modell verfügbar ist. Sie arbeiten immer mit einer Voreinstellung, und AI Studio speichert Ihre Änderungen automatisch." + +-- A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4043921725::INFORMAL"] = "Eine Voreinstellung speichert alle Einstellungen, die du unten vornimmst. So kannst du denselben Vergleich später erneut durchführen, zum Beispiel wenn ein neues Modell verfügbar ist. Du arbeitest immer mit einer Voreinstellung, und AI Studio speichert deine Änderungen automatisch." + +-- First model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4044223184"] = "Erstes Modell" + +-- AI Studio could not save the change to your presets. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4152209182"] = "AI Studio konnte die Änderung an Ihren Voreinstellungen nicht speichern. Bitte versuchen Sie es erneut." + +-- AI Studio could not save the change to your presets. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4152209182::INFORMAL"] = "AI Studio konnte die Änderung an deinen Voreinstellungen nicht speichern. Bitte versuche es erneut." + +-- Collapse +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T5885182"] = "Einklappen" + +-- A preset with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T599249526"] = "Eine Voreinstellung mit diesem Namen ist bereits vorhanden. Bitte wählen Sie einen anderen Namen." + +-- A preset with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T599249526::INFORMAL"] = "Eine Voreinstellung mit diesem Namen ist bereits vorhanden. Bitte wähle einen anderen Namen." + +-- Number of runs +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T752176293"] = "Anzahl der Durchläufe" + +-- Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T764471416"] = "Senden Sie dieselbe Anfrage einmal oder mehrmals an zwei Modelle und vergleichen Sie ihre Antworten nebeneinander. Stimmen Sie blind für die bessere Antwort ab, sehen Sie, wie stark die Antworten voneinander abweichen, und erfahren Sie am Ende, welches Modell welche Antwort verfasst hat." + +-- Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T764471416::INFORMAL"] = "Sende dieselbe Anfrage einmal oder mehrmals an zwei Modelle und vergleiche ihre Antworten nebeneinander. Stimme blind für die bessere Antwort ab, sieh, wie stark die Antworten voneinander abweichen, und erfahre am Ende, welches Modell welche Antwort verfasst hat." + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T867467332"] = "Zweites Modell" + +-- Judge model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T953975257"] = "Richtermodell" + +-- Enter a new name for this comparison series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1211403772"] = "Geben Sie einen neuen Namen für diese Vergleichsreihe ein." + +-- Enter a new name for this comparison series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1211403772::INFORMAL"] = "Gib einen neuen Namen für diese Vergleichsreihe ein." + +-- Open +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1401622761"] = "Öffnen" + +-- Start a new series with this setup +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1452503898"] = "Neue Vergleichsreihe mit diesen Einstellungen starten" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1990305431"] = "Ergebnisse aller Vergleiche" + +-- Delete series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2022370503"] = "Vergleichsreihe löschen" + +-- Switch mode +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2045889708"] = "Modus wechseln" + +-- Name of the series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2083243781"] = "Name der Vergleichsreihe" + +-- Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2398358753"] = "Möchten Sie die Vergleichsreihe „{0}“ wirklich einschließlich aller Chats löschen? Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2398358753::INFORMAL"] = "Möchtest du die Vergleichsreihe „{0}“ wirklich einschließlich aller Chats löschen? Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Your comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T242544328"] = "Ihre Vergleichsreihen" + +-- Your comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T242544328::INFORMAL"] = "Deine Vergleichsreihen" + +-- AI Studio could not rename the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2698444055"] = "AI Studio konnte die Vergleichsreihe nicht umbenennen. Bitte versuchen Sie es erneut." + +-- AI Studio could not rename the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2698444055::INFORMAL"] = "AI Studio konnte die Vergleichsreihe nicht umbenennen. Bitte versuche es erneut." + +-- Back to all series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2733157589"] = "Zurück zu allen Vergleichsreihen" + +-- Started {0}, decided tasks: {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2771954403"] = "Begonnen am {0}, entschiedene Aufgaben: {1}" + +-- Started {0}, decided tasks: {1}, result shown +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2970598131"] = "Begonnen am {0}, entschiedene Aufgaben: {1}, Ergebnis angezeigt" + +-- Rename +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3355849203"] = "Umbenennen" + +-- Rename series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3550827604"] = "Vergleichsreihe umbenennen" + +-- AI Studio could not delete the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3650237868"] = "AI Studio konnte die Vergleichsreihe nicht löschen. Bitte versuchen Sie es erneut." + +-- AI Studio could not delete the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3650237868::INFORMAL"] = "AI Studio konnte die Vergleichsreihe nicht löschen. Bitte versuche es erneut." + +-- You have no comparison series yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T369101477"] = "Sie haben noch keine Vergleichsreihe." + +-- You have no comparison series yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T369101477::INFORMAL"] = "Du hast noch keine Vergleichsreihe." + +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3910956815"] = "Modellvergleich" + +-- New comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T4249192191"] = "Neue Vergleichsreihe" + +-- Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T922224005"] = "Öffnen Sie eine Vergleichsreihe, um eine weitere Aufgabe zu bearbeiten oder sich ihr Ergebnis anzusehen. Die Liste nennt die Modelle nicht, damit Sie nicht erfahren, welches welches ist, bevor Sie sich das Ergebnis ansehen. Wenn Sie dieselben Modelle erneut vergleichen oder eines davon durch ein neues Modell ersetzen möchten, starten Sie eine neue Vergleichsreihe mit den Einstellungen einer bestehenden." + +-- Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T922224005::INFORMAL"] = "Öffne eine Vergleichsreihe, um eine weitere Aufgabe zu bearbeiten oder dir ihr Ergebnis anzusehen. Die Liste nennt die Modelle nicht, damit du nicht erfährst, welches welches ist, bevor du dir das Ergebnis ansiehst. Wenn du dieselben Modelle erneut vergleichen oder eines davon durch ein neues Modell ersetzen möchtest, starte eine neue Vergleichsreihe mit den Einstellungen einer bestehenden." + +-- First token after {0}, complete after {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCAPTION::T1658271318"] = "Erstes Token nach {0}, vollständig nach {1}" + +-- Judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCAPTION::T2094884932"] = "Richter" + +-- This model did not answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCARD::T472607376"] = "Dieses Modell hat nicht geantwortet." + +-- All {0} runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1508408704"] = "Alle {0} Durchläufe sind abgeschlossen." + +-- Start a new request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1767930003"] = "Neue Anfrage starten" + +-- Judge verdicts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1879874010"] = "Urteile des Richters" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1990305431"] = "Ergebnisse aller Vergleiche" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T248601509"] = "Gleich gut" + +-- {0} of {1} runs have no usable judge verdict. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2716994296"] = "Für {0} von {1} Durchläufen liegt kein brauchbares Urteil des Richters vor. Diese Durchläufe zählen oben nicht mit." + +-- {0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2766679987"] = "Für {0} von {1} Durchläufen fehlt Ihre Stimme, weil Sie sie übersprungen haben oder ein Modell nicht geantwortet hat. Diese Durchläufe zählen oben nicht mit." + +-- {0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2766679987::INFORMAL"] = "Für {0} von {1} Durchläufen fehlt deine Stimme, weil du sie übersprungen hast oder ein Modell nicht geantwortet hat. Diese Durchläufe zählen oben nicht mit." + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T360017507"] = "Ihre Stimmen" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T360017507::INFORMAL"] = "Deine Stimmen" + +-- Done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T38691245"] = "Fertig." + +-- Answer A +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1420338340"] = "Antwort A" + +-- Answer B +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1470671197"] = "Antwort B" + +-- You skipped the remaining votes. The summary follows once the remaining runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1964601229"] = "Sie haben die übrigen Stimmen übersprungen. Die Zusammenfassung folgt, sobald die übrigen Durchläufe fertig sind." + +-- You skipped the remaining votes. The summary follows once the remaining runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1964601229::INFORMAL"] = "Du hast die übrigen Stimmen übersprungen. Die Zusammenfassung folgt, sobald die übrigen Durchläufe fertig sind." + +-- Both models are answering. This can take a while. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T2709682697"] = "Beide Modelle antworten gerade. Das kann eine Weile dauern." + +-- Waiting for the next run... +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T758625750"] = "Warten auf den nächsten Durchlauf …" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T1422468824"] = "Ihre Nachrichten im Durchschnitt" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T1422468824::INFORMAL"] = "Deine Nachrichten im Durchschnitt" + +-- Median time +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T2152829620"] = "Zeit (Median)" + +-- Describe your expectations, values, and goals to the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T1734361278"] = "Beschreiben Sie dem Richter Ihre Erwartungen, Werte und Ziele" + +-- Describe your expectations, values, and goals to the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T1734361278::INFORMAL"] = "Beschreibe dem Richter deine Erwartungen, Werte und Ziele" + +-- Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2577739352"] = "Bitte beschreiben Sie dem Richter Ihre Erwartungen, Werte und Ziele, oder bestätigen Sie, dass eine KI für Sie entscheiden darf, was gut und richtig ist." + +-- Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2577739352::INFORMAL"] = "Bitte beschreibe dem Richter deine Erwartungen, Werte und Ziele, oder bestätige, dass eine KI für dich entscheiden darf, was gut und richtig ist." + +-- Your expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2641486288"] = "Ihre Erwartungen, Werte und Ziele" + +-- Your expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2641486288::INFORMAL"] = "Deine Erwartungen, Werte und Ziele" + +-- Yes, an AI may decide for me what is good and right +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2805496671"] = "Ja, eine KI darf für mich entscheiden, was gut und richtig ist" + +-- For example: I prefer short answers which get to the point. Technical terms are welcome, since I work in the field. An answer should weigh several views instead of taking a side. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T326873300"] = "Zum Beispiel: Ich bevorzuge kurze Antworten, die auf den Punkt kommen. Fachbegriffe sind willkommen, ich bin vom Fach. Eine Antwort soll mehrere Sichtweisen abwägen, statt Partei zu ergreifen." + +-- Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T332317276"] = "Ohne Ihre Erwartungen, Werte und Ziele urteilt der Richter nach seinen eigenen Maßstäben: nach dem, was er im Training gelernt hat, mit dem Bias derer, die ihn trainiert haben. Seine Urteile sagen Ihnen dann, was diese KI für gut und richtig hält, nicht, was Sie dafür halten." + +-- Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T332317276::INFORMAL"] = "Ohne deine Erwartungen, Werte und Ziele urteilt der Richter nach seinen eigenen Maßstäben: nach dem, was er im Training gelernt hat, mit dem Bias derer, die ihn trainiert haben. Seine Urteile sagen dir dann, was diese KI für gut und richtig hält, nicht, was du dafür hältst." + +-- I confirm that I have read and understood the above +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T3683380716"] = "Ich bestätige, dass ich das oben Genannte gelesen und verstanden habe" + +-- No, I will describe my expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T401061652"] = "Nein, ich beschreibe meine Erwartungen, Werte und Ziele" + +-- What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T588888121"] = "Was eine gute Antwort ist, entscheiden Sie, nicht eine KI. Ohne Ihre Vorgaben urteilt der Richter nach dem, was er im Training gelernt hat: nach den Werten, Vorlieben und dem Bias derer, die ihn trainiert haben, etwa eines Anbieters in den USA oder in China. Ist eine lange, ausführliche Antwort besser als eine kurze? Soll eine Antwort ihre Quellen nennen? Soll sie Partei ergreifen oder mehrere Sichtweisen abwägen? Das können nur Sie sagen. Beschreiben Sie dem Richter deshalb so genau wie möglich, was Sie erwarten, was Ihnen wichtig ist und was Sie erreichen möchten. Wir empfehlen Ihnen das dringend: Sonst sagen die Urteile mehr über den Richter als über Ihre Modelle." + +-- What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T588888121::INFORMAL"] = "Was eine gute Antwort ist, entscheidest du, nicht eine KI. Ohne deine Vorgaben urteilt der Richter nach dem, was er im Training gelernt hat: nach den Werten, Vorlieben und dem Bias derer, die ihn trainiert haben, etwa eines Anbieters in den USA oder in China. Ist eine lange, ausführliche Antwort besser als eine kurze? Soll eine Antwort ihre Quellen nennen? Soll sie Partei ergreifen oder mehrere Sichtweisen abwägen? Das kannst nur du sagen. Beschreibe dem Richter deshalb so genau wie möglich, was du erwartest, was dir wichtig ist und was du erreichen möchtest. Wir empfehlen dir das dringend: Sonst sagen die Urteile mehr über den Richter als über deine Modelle." + +-- The judge saw both answers as equally good, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T110452334"] = "Der Richter hielt beide Antworten für gleich gut, wie Sie selbst." + +-- The judge saw both answers as equally good, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T110452334::INFORMAL"] = "Der Richter hielt beide Antworten für gleich gut, wie du selbst." + +-- The judge preferred {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1418012290"] = "Der Richter bevorzugte {0}." + +-- The judge preferred {0}, differing from your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1499258629"] = "Der Richter bevorzugte {0}, anders als Sie." + +-- The judge preferred {0}, differing from your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1499258629::INFORMAL"] = "Der Richter bevorzugte {0}, anders als du." + +-- The judge saw both answers as equally good. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1679196260"] = "Der Richter hielt beide Antworten für gleich gut." + +-- The judge preferred {0}, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T2457939612"] = "Der Richter bevorzugte {0}, wie Sie selbst." + +-- The judge preferred {0}, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T2457939612::INFORMAL"] = "Der Richter bevorzugte {0}, wie du selbst." + +-- The judge saw both answers as equally good, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T3189061594"] = "Der Richter hielt beide Antworten für gleich gut, während Sie eine davon bevorzugt haben." + +-- The judge saw both answers as equally good, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T3189061594::INFORMAL"] = "Der Richter hielt beide Antworten für gleich gut, während du eine davon bevorzugt hast." + +-- Judge: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T4194679568"] = "Richter: {0}" + +-- The judge did not give a usable verdict. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T4278818434"] = "Der Richter hat kein brauchbares Urteil abgegeben." + +-- Compare two models without knowing which one answers. Choose what you want to find out. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1060503021"] = "Vergleichen Sie zwei Modelle, ohne zu wissen, welches Modell antwortet. Wählen Sie aus, was Sie herausfinden möchten." + +-- Compare two models without knowing which one answers. Choose what you want to find out. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1060503021::INFORMAL"] = "Vergleiche zwei Modelle, ohne zu wissen, welches Modell antwortet. Wähle aus, was du herausfinden möchtest." + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1990305431"] = "Ergebnisse aller Vergleiche" + +-- Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T2256226870"] = "Senden Sie dieselbe Anfrage einmal oder mehrmals hintereinander an beide Modelle und stimmen Sie jedes Mal blind für die bessere Antwort ab. So erfahren Sie, welches Modell besser antwortet und wie stark seine Antworten von Durchlauf zu Durchlauf schwanken. Optional gibt ein Richtermodell sein eigenes Urteil ab. Am Ende sehen Sie, welches Modell welche Antwort verfasst hat." + +-- Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T2256226870::INFORMAL"] = "Sende dieselbe Anfrage einmal oder mehrmals hintereinander an beide Modelle und stimme jedes Mal blind für die bessere Antwort ab. So erfährst du, welches Modell besser antwortet und wie stark seine Antworten von Durchlauf zu Durchlauf schwanken. Optional gibt ein Richtermodell sein eigenes Urteil ab. Am Ende siehst du, welches Modell welche Antwort verfasst hat." + +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T3910956815"] = "Modellvergleich" + +-- Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T4217745718"] = "Arbeiten Sie mit einem der beiden Modelle an einer echten Aufgabe aus Ihrem Arbeitsalltag – mit Rückfragen, Werkzeugen und Datenquellen. Bearbeiten Sie anschließend dieselbe Aufgabe mit dem anderen Modell. Danach entscheiden Sie, welcher Chat Ihnen mehr geholfen hat, ohne zu wissen, welches Modell dahintersteckt. Jede Aufgabe fließt über Tage oder Wochen hinweg in das Ergebnis ein." + +-- Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T4217745718::INFORMAL"] = "Arbeite mit einem der beiden Modelle an einer echten Aufgabe aus deinem Arbeitsalltag – mit Rückfragen, Werkzeugen und Datenquellen. Bearbeite anschließend dieselbe Aufgabe mit dem anderen Modell. Danach entscheidest du, welcher Chat dir mehr geholfen hat, ohne zu wissen, welches Modell dahintersteckt. Jede Aufgabe fließt über Tage oder Wochen hinweg in das Ergebnis ein." + +-- Model Comparison: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T2204151393"] = "Modellvergleich: {0}" + +-- Everyday Tasks +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T2633045292"] = "Alltagsaufgaben" + +-- Answer Variance +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T3575446502"] = "Antwortvarianz" + +-- A series with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T1933751712"] = "Eine Vergleichsreihe mit diesem Namen ist bereits vorhanden. Bitte wählen Sie einen anderen Namen." + +-- A series with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T1933751712::INFORMAL"] = "Eine Vergleichsreihe mit diesem Namen ist bereits vorhanden. Bitte wähle einen anderen Namen." + +-- Please give the series a name, so that you find it again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T2512778872"] = "Bitte geben Sie der Vergleichsreihe einen Namen, damit Sie sie wiederfinden." + +-- Please give the series a name, so that you find it again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T2512778872::INFORMAL"] = "Bitte gib der Vergleichsreihe einen Namen, damit du sie wiederfindest." + +-- The name of your series may have at most {0} characters. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T4001587725"] = "Der Name Ihrer Vergleichsreihe darf höchstens {0} Zeichen lang sein." + +-- The name of your series may have at most {0} characters. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T4001587725::INFORMAL"] = "Der Name deiner Vergleichsreihe darf höchstens {0} Zeichen lang sein." + +-- {0} is clearly better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1539751573"] = "{0} ist eindeutig besser" + +-- Stored comparisons: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1864825956"] = "Gespeicherte Vergleiche: {0}" + +-- Judge verdicts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1879874010"] = "Urteile der Richter" + +-- Assessment +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T2135313999"] = "Einschätzung" + +-- {0} vs. {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T2197341449"] = "{0} gegen {1}" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T248601509"] = "Gleich gut" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T360017507"] = "Ihre Stimmen" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T360017507::INFORMAL"] = "Deine Stimmen" + +-- Not yet clear +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T877424713"] = "Noch nicht eindeutig" + +-- The use of this provider has been prohibited by your organization. Please select a different provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1807387355"] = "Die Verwendung dieses Anbieters wurde von Ihrer Organisation untersagt. Bitte wählen Sie einen anderen Anbieter." + +-- The use of this provider has been prohibited by your organization. Please select a different provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1807387355::INFORMAL"] = "Die Verwendung dieses Anbieters wurde von deiner Organisation untersagt. Bitte wähle einen anderen Anbieter." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1809312323"] = "Bitte wählen Sie einen Anbieter aus." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1809312323::INFORMAL"] = "Bitte wähle einen Anbieter aus." + +-- This provider does not meet the confidence requirements of this assistant. Please choose another one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T512106357"] = "Dieser Anbieter erfüllt die Vertrauensanforderungen dieses Assistenten nicht. Bitte wählen Sie einen anderen Anbieter." + +-- This provider does not meet the confidence requirements of this assistant. Please choose another one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T512106357::INFORMAL"] = "Dieser Anbieter erfüllt die Vertrauensanforderungen dieses Assistenten nicht. Bitte wähle einen anderen Anbieter." + +-- Clearly worse +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T1738164753"] = "Eindeutig schlechter" + +-- Assessment +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2135313999"] = "Einschätzung" + +-- Wins +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2168528702"] = "Siege" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2189814010"] = "Modell" + +-- {0} to {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2854601881"] = "{0} bis {1}" + +-- Losses +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3417493210"] = "Niederlagen" + +-- Clearly better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3673745789"] = "Eindeutig besser" + +-- Ties +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3724044438"] = "Unentschieden" + +-- Share of wins +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3818681848"] = "Anteil der Siege" + +-- Likely range +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T554551492"] = "Wahrscheinlicher Bereich" + +-- Not yet clear +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T877424713"] = "Noch nicht eindeutig" + +-- “Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T1602691513"] = "„Verglichen“ zählt die Durchläufe und Aufgaben, für die sowohl Ihre Stimme als auch ein Urteil des Richters vorliegen, „Gleiche Wahl“ diejenigen, in denen der Richter dasselbe gewählt hat wie Sie, und „Übereinstimmung“ gibt deren Anteil an. Je höher er ist, desto eher entscheidet der Richter wie Sie und desto mehr sagen seine Urteile dort aus, wo Sie selbst nicht abgestimmt haben." + +-- “Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T1602691513::INFORMAL"] = "„Verglichen“ zählt die Durchläufe und Aufgaben, für die sowohl deine Stimme als auch ein Urteil des Richters vorliegen, „Gleiche Wahl“ diejenigen, in denen der Richter dasselbe gewählt hat wie du, und „Übereinstimmung“ gibt deren Anteil an. Je höher er ist, desto eher entscheidet der Richter wie du und desto mehr sagen seine Urteile dort aus, wo du selbst nicht abgestimmt hast." + +-- The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2051424113"] = "Der Aufwand zeigt Ihnen, wie viel Ihnen jedes Modell abverlangt hat, bis Sie mit einer Aufgabe fertig waren: wie viele Nachrichten Sie im Durchschnitt geschrieben haben, Ihre erste mitgezählt, und wie lange Sie mit dem Modell gearbeitet haben. Für die Zeit nimmt AI Studio den Median: Die Hälfte der Aufgaben dauerte kürzer, die andere Hälfte länger. So verzerrt ein über Nacht offener Chat das Bild nicht. Weniger Nachrichten und weniger Zeit können bedeuten, dass ein Modell Sie schneller verstanden hat." + +-- The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2051424113::INFORMAL"] = "Der Aufwand zeigt dir, wie viel dir jedes Modell abverlangt hat, bis du mit einer Aufgabe fertig warst: wie viele Nachrichten du im Durchschnitt geschrieben hast, deine erste mitgezählt, und wie lange du mit dem Modell gearbeitet hast. Für die Zeit nimmt AI Studio den Median: Die Hälfte der Aufgaben dauerte kürzer, die andere Hälfte länger. So verzerrt ein über Nacht offener Chat das Bild nicht. Weniger Nachrichten und weniger Zeit können bedeuten, dass ein Modell dich schneller verstanden hat." + +-- The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2144465448"] = "Das Ergebnis berücksichtigt nur die Aufgaben, bei denen Sie entschieden haben, ohne zu wissen, von welchen Modellen die Antworten stammen. Es zeigt, wie oft Ihnen jedes Modell mehr geholfen hat, ob das deutlich über dem Zufallsniveau liegt und wie viel Aufwand jedes Modell Sie gekostet hat." + +-- The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2144465448::INFORMAL"] = "Das Ergebnis berücksichtigt nur die Aufgaben, bei denen du entschieden hast, ohne zu wissen, von welchen Modellen die Antworten stammen. Es zeigt, wie oft dir jedes Modell mehr geholfen hat, ob das deutlich über dem Zufallsniveau liegt und wie viel Aufwand jedes Modell dich gekostet hat." + +-- The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2456227034"] = "Die Spalten folgen der Reihenfolge in der Überschrift: das erste Modell, beide gleich gut, das zweite Modell. Die Einschätzung sagt, ob eines der beiden eindeutig besser ist. Das ist nur dann der Fall, wenn der wahrscheinliche Bereich seines Anteils der Siege bei einer Sicherheit von 95 Prozent vollständig über der Hälfte liegt; Unentschieden zählen für diesen Anteil nicht." + +-- The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2456227034::INFORMAL"] = "Die Spalten folgen der Reihenfolge in der Überschrift: das erste Modell, beide gleich gut, das zweite Modell. Die Einschätzung sagt, ob eines der beiden eindeutig besser ist. Das ist nur dann der Fall, wenn der wahrscheinliche Bereich seines Anteils der Siege bei einer Sicherheit von 95 Prozent vollständig über der Hälfte liegt; Unentschieden zählen für diesen Anteil nicht." + +-- The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T3079420033"] = "Der Anteil der Siege berücksichtigt nur die Durchläufe und Aufgaben, die eines der Modelle gewonnen hat, nicht die Unentschieden. Bei wenigen davon spielt der Zufall eine große Rolle. Deshalb gilt ein Modell nur dann als eindeutig besser oder schlechter, wenn der wahrscheinliche Bereich seines Anteils bei einer Sicherheit von 95 Prozent vollständig über oder unter der Hälfte liegt. Viele Durchläufe derselben Anfrage sagen viel über diese Anfrage aus, aber wenig über andere. Vergleichen Sie die Modelle daher auch anhand verschiedener Anfragen und Aufgaben." + +-- The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T3079420033::INFORMAL"] = "Der Anteil der Siege berücksichtigt nur die Durchläufe und Aufgaben, die eines der Modelle gewonnen hat, nicht die Unentschieden. Bei wenigen davon spielt der Zufall eine große Rolle. Deshalb gilt ein Modell nur dann als eindeutig besser oder schlechter, wenn der wahrscheinliche Bereich seines Anteils bei einer Sicherheit von 95 Prozent vollständig über oder unter der Hälfte liegt. Viele Durchläufe derselben Anfrage sagen viel über diese Anfrage aus, aber wenig über andere. Vergleiche die Modelle daher auch anhand verschiedener Anfragen und Aufgaben." + +-- Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T584847906"] = "Hier sehen Sie, wie Ihre Stimmen und die Urteile des Richters für diese Anfrage ausgefallen sind. Eine einzelne Anfrage sagt wenig darüber aus, wie gut die Modelle im Allgemeinen abschneiden: Vergleichen Sie sie anhand mehrerer Anfragen und bei echten Aufgaben im Modus „Alltagsaufgaben“, bevor Sie sich für eines entscheiden. Die Ergebnisse aller Vergleiche führen alles zusammen." + +-- Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T584847906::INFORMAL"] = "Hier siehst du, wie deine Stimmen und die Urteile des Richters für diese Anfrage ausgefallen sind. Eine einzelne Anfrage sagt dir wenig darüber aus, wie gut die Modelle im Allgemeinen abschneiden: Vergleiche sie anhand mehrerer Anfragen und bei echten Aufgaben im Modus „Alltagsaufgaben“, bevor du dich für eines entscheidest. Die Ergebnisse aller Vergleiche führen alles zusammen." + +-- Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T770148061"] = "Hier sehen Sie, was alle Ihre Vergleiche über die Modelle aussagen. Aus dem Modus „Antwortvarianz“ fließen Ihre Stimmen zu den Antworten zweier Modelle auf dieselbe Anfrage ein, aus dem Modus „Alltagsaufgaben“ Ihre Entscheidungen bei echten Aufgaben. Eine Vergleichsreihe zählt erst, nachdem Sie sich ihr Ergebnis angesehen haben, und nur mit den Aufgaben, die Sie davor entschieden haben, als Sie die Modelle noch nicht kannten. Wählen Sie oben, ob die Ergebnisse beider Modi zählen oder nur die eines Modus." + +-- Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T770148061::INFORMAL"] = "Hier siehst du, was alle deine Vergleiche über die Modelle aussagen. Aus dem Modus „Antwortvarianz“ fließen deine Stimmen zu den Antworten zweier Modelle auf dieselbe Anfrage ein, aus dem Modus „Alltagsaufgaben“ deine Entscheidungen bei echten Aufgaben. Eine Vergleichsreihe zählt erst, nachdem du dir ihr Ergebnis angesehen hast, und nur mit den Aufgaben, die du davor entschieden hast, als du die Modelle noch nicht kanntest. Wähle oben, ob die Ergebnisse beider Modi zählen oder nur die eines Modus." + +-- Run {0}: without your vote +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1440259155"] = "Durchlauf {0}: ohne Ihre Stimme" + +-- Run {0}: without your vote +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1440259155::INFORMAL"] = "Durchlauf {0}: ohne deine Stimme" + +-- Run {0}: you found both answers equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1728842697"] = "Durchlauf {0}: Sie haben beide Antworten gleich gut gefunden" + +-- Run {0}: you found both answers equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1728842697::INFORMAL"] = "Durchlauf {0}: Du hast beide Antworten gleich gut gefunden" + +-- Run {0}: at least one model did not answer +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1775562627"] = "Durchlauf {0}: Mindestens ein Modell hat nicht geantwortet" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T248601509"] = "Gleich gut" + +-- First model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4044223184"] = "Erstes Modell" + +-- Run {0}: you preferred {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4293630979"] = "Durchlauf {0}: Sie haben {1} bevorzugt" + +-- Run {0}: you preferred {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4293630979::INFORMAL"] = "Durchlauf {0}: Du hast {1} bevorzugt" + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T867467332"] = "Zweites Modell" + +-- Run {0}: the judge gave no usable verdict, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1221245882"] = "Durchlauf {0}: Der Richter hat kein brauchbares Urteil abgegeben, Ihre Stimme fehlt." + +-- Run {0}: the judge gave no usable verdict, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1221245882::INFORMAL"] = "Durchlauf {0}: Der Richter hat kein brauchbares Urteil abgegeben, deine Stimme fehlt." + +-- Run {0}: no usable answer, nothing to vote on. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1407798402"] = "Durchlauf {0}: keine brauchbare Antwort, nichts zum Abstimmen." + +-- Run {0}: queued, waiting for a free slot. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1600973378"] = "Durchlauf {0}: in der Warteschlange, wartet auf einen freien Platz." + +-- Run {0}: still working. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1838060603"] = "Durchlauf {0}: läuft noch." + +-- Run {0}: the models and the judge are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2115467037"] = "Durchlauf {0}: Die Modelle und der Richter sind fertig, Ihre Stimme fehlt noch." + +-- Run {0}: the models and the judge are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2115467037::INFORMAL"] = "Durchlauf {0}: Die Modelle und der Richter sind fertig, deine Stimme fehlt noch." + +-- Progress of the runs +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2330056762"] = "Fortschritt der Durchläufe" + +-- Run {0}: your vote was skipped. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2715329048"] = "Durchlauf {0}: Ihre Stimme wurde übersprungen." + +-- Run {0}: your vote was skipped. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2715329048::INFORMAL"] = "Durchlauf {0}: Deine Stimme wurde übersprungen." + +-- Run {0}: voted on. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T3789682551"] = "Durchlauf {0}: abgestimmt." + +-- Run {0}: the models are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T4197671504"] = "Durchlauf {0}: Die Modelle sind fertig, Ihre Stimme fehlt noch." + +-- Run {0}: the models are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T4197671504::INFORMAL"] = "Durchlauf {0}: Die Modelle sind fertig, deine Stimme fehlt noch." + +-- Chat 1 helped more +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2438409751"] = "Chat 1 war hilfreicher" + +-- Chat 1 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2553260352"] = "Chat 1" + +-- Chat 2 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2603593209"] = "Chat 2" + +-- Both helped equally +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3093762271"] = "Beide haben gleich gut geholfen" + +-- Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3870350915"] = "Welcher Chat hat Ihnen bei Ihrer Aufgabe mehr geholfen? Sehen Sie sich bei Bedarf beide Chats noch einmal an. Sie wissen weiterhin nicht, welches Modell in welchem Chat geantwortet hat." + +-- Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3870350915::INFORMAL"] = "Welcher Chat hat dir bei deiner Aufgabe mehr geholfen? Sieh dir bei Bedarf beide Chats noch einmal an. Du weißt weiterhin nicht, welches Modell in welchem Chat geantwortet hat." + +-- Chat 2 helped more +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T988417422"] = "Chat 2 war hilfreicher" + +-- Tasks decided without knowing the models: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1092985245"] = "Ohne Kenntnis der Modelle entschiedene Aufgaben: {0}" + +-- The judge chose the same as you in {0} of {1} tasks ({2}). +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T118785941"] = "Der Richter hat in {0} von {1} Aufgaben genauso entschieden wie Sie ({2})." + +-- The judge chose the same as you in {0} of {1} tasks ({2}). +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T118785941::INFORMAL"] = "Der Richter hat in {0} von {1} Aufgaben genauso entschieden wie du ({2})." + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1422468824"] = "Ihre Nachrichten im Durchschnitt" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1422468824::INFORMAL"] = "Deine Nachrichten im Durchschnitt" + +-- After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1709474261"] = "Nachdem Sie das Ergebnis gesehen hatten, haben Sie noch {0} weitere Aufgaben entschieden: {1} gewann {2}, {3} gewann {4}, und {5} gingen unentschieden aus. Diese Aufgaben zählen oben nicht mit, da Sie die Modelle kannten." + +-- After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1709474261::INFORMAL"] = "Nachdem du das Ergebnis gesehen hattest, hast du noch {0} weitere Aufgaben entschieden: {1} gewann {2}, {3} gewann {4}, und {5} gingen unentschieden aus. Diese Aufgaben zählen oben nicht mit, da du die Modelle kanntest." + +-- Result of the series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1779013167"] = "Ergebnis der Vergleichsreihe" + +-- Your effort with each model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1872744172"] = "Ihr Aufwand je Modell" + +-- Your effort with each model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1872744172::INFORMAL"] = "Dein Aufwand je Modell" + +-- Median time +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2152829620"] = "Zeit (Median)" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2189814010"] = "Modell" + +-- The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2579182820"] = "Das Modell im ersten Chat hat {0} dieser Aufgaben gewonnen, das Modell im zweiten Chat {1}. Wenn der zweite Chat deutlich häufiger gewinnt, könnte es Ihnen geholfen haben, dass Sie die Aufgabe dort besser kannten." + +-- The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2579182820::INFORMAL"] = "Das Modell im ersten Chat hat {0} dieser Aufgaben gewonnen, das Modell im zweiten Chat {1}. Wenn der zweite Chat deutlich häufiger gewinnt, könnte es dir geholfen haben, dass du die Aufgabe dort besser kanntest." + +-- Decided tasks +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3224295221"] = "Entschiedene Aufgaben" + +-- You decided no task of this series without knowing the models, so it has no result. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3510794091"] = "Sie haben in dieser Vergleichsreihe keine Aufgabe ohne Kenntnis der Modelle entschieden, daher gibt es kein Ergebnis." + +-- You decided no task of this series without knowing the models, so it has no result. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3510794091::INFORMAL"] = "Du hast in dieser Vergleichsreihe keine Aufgabe ohne Kenntnis der Modelle entschieden, daher gibt es kein Ergebnis." + +-- classic RAG +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1197693637"] = "klassisches RAG" + +-- Profile: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1311539038"] = "Profil: {0}" + +-- Data sources: {0} ({1}) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1413816655"] = "Datenquellen: {0} ({1})" + +-- No profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2028602035"] = "Kein Profil" + +-- No data sources +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2502582508"] = "Keine Datenquellen" + +-- No tools +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2868032641"] = "Keine Werkzeuge" + +-- Profile: no longer available +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3128799223"] = "Profil: nicht mehr verfügbar" + +-- {0} no longer available +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3209274018"] = "{0} nicht mehr verfügbar" + +-- Tools: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3940425864"] = "Werkzeuge: {0}" + +-- Semantic Search +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T853993291"] = "Semantische Suche" + +-- After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T1606451454"] = "Nachdem Sie selbst über eine Aufgabe abgestimmt haben, liest ein drittes Modell beide Chats und gibt ein eigenes Urteil ab, ohne zu wissen, welches Modell welchen Chat geschrieben hat. Der Richter legt Ihre Erwartungen, Werte und Ziele an jede Aufgabe gleich an und gibt Ihnen so eine zweite Meinung; im Ergebnis der Vergleichsreihe sehen Sie, wie oft er Ihnen zugestimmt hat. Da er beide Chats vollständig liest, muss er auch Ihre Datenquellen sehen dürfen. Er kostet eine zusätzliche Anfrage pro Aufgabe." + +-- After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T1606451454::INFORMAL"] = "Nachdem du selbst über eine Aufgabe abgestimmt hast, liest ein drittes Modell beide Chats und gibt ein eigenes Urteil ab, ohne zu wissen, welches Modell welchen Chat geschrieben hat. Der Richter legt deine Erwartungen, Werte und Ziele an jede Aufgabe gleich an und gibt dir so eine zweite Meinung; im Ergebnis der Vergleichsreihe siehst du, wie oft er dir zugestimmt hat. Da er beide Chats vollständig liest, muss er auch deine Datenquellen sehen dürfen. Er kostet eine zusätzliche Anfrage pro Aufgabe." + +-- Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T2709626367"] = "Beide Modelle verwenden dasselbe Profil, dieselben Werkzeuge und dieselben Datenquellen, damit der Vergleich fair bleibt. AI Studio bietet daher nur an, was beide Modelle nutzen dürfen: Das Modell mit dem geringeren Vertrauensniveau bestimmt, welche Werkzeuge und Datenquellen Sie auswählen können, und die Werkzeugauswahl führt die übrigen mit Begründung auf. Wenn beide Modelle die Datenquellen selbst durchsuchen können, nutzen sie dafür die semantische Suche. Andernfalls durchsucht AI Studio die Datenquellen für beide Modelle bei jeder Nachricht. Bei jeder Aufgabe können Sie Ihrer Nachricht Dateien anhängen, die beide Modelle erhalten – Bilder jedoch nur, wenn beide Modelle sie lesen können." + +-- Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T2709626367::INFORMAL"] = "Beide Modelle verwenden dasselbe Profil, dieselben Werkzeuge und dieselben Datenquellen, damit der Vergleich fair bleibt. AI Studio bietet daher nur an, was beide Modelle nutzen dürfen: Das Modell mit dem geringeren Vertrauensniveau bestimmt, welche Werkzeuge und Datenquellen du auswählen kannst, und die Werkzeugauswahl führt die übrigen mit Begründung auf. Wenn beide Modelle die Datenquellen selbst durchsuchen können, nutzen sie dafür die semantische Suche. Andernfalls durchsucht AI Studio die Datenquellen für beide Modelle bei jeder Nachricht. Bei jeder Aufgabe kannst du deiner Nachricht Dateien anhängen, die beide Modelle erhalten – Bilder jedoch nur, wenn beide Modelle sie lesen können." + +-- Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T326613437"] = "Wählen Sie die beiden Modelle aus, die Sie vergleichen möchten, zum Beispiel das Modell, mit dem Sie täglich arbeiten, und ein neues Modell. Bei jeder Aufgabe entscheidet AI Studio zufällig, mit welchem Modell Sie zuerst chatten. Die Namen der Modelle bleiben verborgen, bis Sie sich das Ergebnis der Reihe ansehen." + +-- Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T326613437::INFORMAL"] = "Wähle die beiden Modelle aus, die du vergleichen möchtest, zum Beispiel das Modell, mit dem du täglich arbeitest, und ein neues Modell. Bei jeder Aufgabe entscheidet AI Studio zufällig, mit welchem Modell du zuerst chattest. Die Namen der Modelle bleiben verborgen, bis du dir das Ergebnis der Reihe ansiehst." + +-- A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T49281341"] = "Eine Aufgabe läuft in vier Schritten ab. Zuerst starten Sie die Aufgabe, und AI Studio wählt zufällig aus, mit welchem der beiden Modelle Sie zuerst chatten. Dann bearbeiten Sie Ihre Aufgabe wie gewohnt im Chat: Sie können Dateien anhängen, und das Modell verwendet das Profil, die Werkzeuge und die Datenquellen der Vergleichsreihe. Sobald Sie fertig sind, erhält das andere Modell Ihre erste Nachricht samt Dateien, und Sie setzen dieselbe Aufgabe mit ihm fort. Zuletzt entscheiden Sie, welcher Chat Ihnen mehr geholfen hat; hat die Vergleichsreihe einen Richter, gibt er danach sein eigenes Urteil ab. Das Ergebnis der Vergleichsreihe können Sie sich jederzeit ansehen. Da es die Modelle nennt, gelten die Aufgaben, die Sie danach entscheiden, nicht mehr als blind." + +-- A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T49281341::INFORMAL"] = "Eine Aufgabe läuft in vier Schritten ab. Zuerst startest du die Aufgabe, und AI Studio wählt zufällig aus, mit welchem der beiden Modelle du zuerst chattest. Dann bearbeitest du deine Aufgabe wie gewohnt im Chat: Du kannst Dateien anhängen, und das Modell verwendet das Profil, die Werkzeuge und die Datenquellen der Vergleichsreihe. Sobald du fertig bist, erhält das andere Modell deine erste Nachricht samt Dateien, und du setzt dieselbe Aufgabe mit ihm fort. Zuletzt entscheidest du, welcher Chat dir mehr geholfen hat; hat die Vergleichsreihe einen Richter, gibt er danach sein eigenes Urteil ab. Das Ergebnis der Vergleichsreihe kannst du dir jederzeit ansehen. Da es die Modelle nennt, gelten die Aufgaben, die du danach entscheidest, nicht mehr als blind." + +-- A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T863629402"] = "Eine Vergleichsreihe sammelt Ihre tatsächlichen Aufgaben über Tage oder Wochen hinweg. Sie bearbeiten jede Aufgabe zunächst mit einem der beiden Modelle und anschließend dieselbe Aufgabe mit dem anderen. Der zweite Chat beginnt mit Ihrer ersten Nachricht aus dem ersten Chat. Danach entscheiden Sie, welcher Chat Ihnen mehr geholfen hat. Erst wenn Sie sich das Ergebnis der Reihe ansehen, erfahren Sie, welches Modell welches war." + +-- A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T863629402::INFORMAL"] = "Eine Vergleichsreihe sammelt deine tatsächlichen Aufgaben über Tage oder Wochen hinweg. Du bearbeitest jede Aufgabe zunächst mit einem der beiden Modelle und anschließend dieselbe Aufgabe mit dem anderen. Der zweite Chat beginnt mit deiner ersten Nachricht aus dem ersten Chat. Danach entscheidest du, welcher Chat dir mehr geholfen hat. Erst wenn du dir das Ergebnis der Reihe ansiehst, erfährst du, welches Modell welches war." + +-- The judge has not judged this task yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T126436094"] = "Der Richter hat diese Aufgabe noch nicht beurteilt." + +-- The judge preferred chat {0}, while you preferred the other one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1611643242"] = "Der Richter bevorzugte Chat {0}, Sie dagegen den anderen." + +-- The judge preferred chat {0}, while you preferred the other one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1611643242::INFORMAL"] = "Der Richter bevorzugte Chat {0}, du dagegen den anderen." + +-- Judge on task {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1798480084"] = "Richter zu Aufgabe {0}" + +-- The judge found both chats equally helpful, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1811336012"] = "Der Richter fand beide Chats gleich hilfreich, während Sie einen davon bevorzugt haben." + +-- The judge found both chats equally helpful, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1811336012::INFORMAL"] = "Der Richter fand beide Chats gleich hilfreich, während du einen davon bevorzugt hast." + +-- The judge reads both chats. This can take a while. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T185857423"] = "Der Richter liest beide Chats. Das kann eine Weile dauern." + +-- The judge preferred chat {0}, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T2805496490"] = "Der Richter bevorzugte Chat {0}, genau wie Sie." + +-- The judge preferred chat {0}, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T2805496490::INFORMAL"] = "Der Richter bevorzugte Chat {0}, genau wie du." + +-- Ask the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3611576738"] = "Richter fragen" + +-- The judge preferred chat {0}, while you found both chats equally helpful. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3944557605"] = "Der Richter bevorzugte Chat {0}, während Sie beide Chats gleich hilfreich gefunden haben." + +-- The judge preferred chat {0}, while you found both chats equally helpful. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3944557605::INFORMAL"] = "Der Richter bevorzugte Chat {0}, während du beide Chats gleich hilfreich gefunden hast." + +-- Both chats together are too long for the judge, so it did not judge this task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4003597607"] = "Beide Chats zusammen sind zu lang für den Richter, daher hat er diese Aufgabe nicht beurteilt." + +-- The judge is no longer available as it was when the series began, so it did not judge this task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4173102270"] = "Der Richter ist nicht mehr so verfügbar wie zu Beginn der Vergleichsreihe, daher hat er diese Aufgabe nicht beurteilt." + +-- The judge did not give a usable verdict. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4278818434"] = "Der Richter hat kein brauchbares Urteil abgegeben." + +-- The judge found both chats equally helpful, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T970331360"] = "Der Richter fand beide Chats gleich hilfreich, genau wie Sie." + +-- The judge found both chats equally helpful, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T970331360::INFORMAL"] = "Der Richter fand beide Chats gleich hilfreich, genau wie du." + +-- Show the result +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1015029476"] = "Ergebnis anzeigen" + +-- Test another task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1029988487"] = "Weitere Aufgabe testen" + +-- Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1119857700"] = "Arbeiten Sie jetzt mit dem anderen Modell an derselben Aufgabe weiter. Es hat Ihre erste Nachricht bereits erhalten. Wenn Sie fertig sind, entscheiden Sie, welcher Chat Ihnen mehr geholfen hat." + +-- Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1119857700::INFORMAL"] = "Arbeite jetzt mit dem anderen Modell an derselben Aufgabe weiter. Es hat deine erste Nachricht bereits erhalten. Wenn du fertig bist, entscheidest du, welcher Chat dir mehr geholfen hat." + +-- One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T116035530"] = "Eines der beiden Modelle ist nicht mehr so verfügbar wie zu Beginn der Reihe: Sein Anbieter wurde gelöscht, oder er verwendet inzwischen ein anderes Modell. Sie können keine weitere Aufgabe in dieser Reihe bearbeiten, aber eine neue Reihe mit ihren Einstellungen starten und das Modell neu auswählen." + +-- One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T116035530::INFORMAL"] = "Eines der beiden Modelle ist nicht mehr so verfügbar wie zu Beginn der Reihe: Sein Anbieter wurde gelöscht, oder er verwendet inzwischen ein anderes Modell. Du kannst keine weitere Aufgabe in dieser Reihe bearbeiten, aber eine neue Reihe mit ihren Einstellungen starten und das Modell neu auswählen." + +-- You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1163964876"] = "Sie haben das Ergebnis dieser Vergleichsreihe bereits gesehen und wissen daher, welches Modell in welchem Chat antwortet. Eine weitere Aufgabe gilt deshalb nicht mehr als blind: Das Ergebnis weist sie getrennt aus, und die Ergebnisse aller Vergleiche lassen sie weg. Um wieder blind zu vergleichen, starten Sie eine neue Vergleichsreihe mit den Einstellungen dieser Reihe. Möchten Sie trotzdem eine weitere Aufgabe in dieser Vergleichsreihe testen?" + +-- You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1163964876::INFORMAL"] = "Du hast das Ergebnis dieser Vergleichsreihe bereits gesehen und weißt daher, welches Modell in welchem Chat antwortet. Eine weitere Aufgabe gilt deshalb nicht mehr als blind: Das Ergebnis weist sie getrennt aus, und die Ergebnisse aller Vergleiche lassen sie weg. Um wieder blind zu vergleichen, starte eine neue Vergleichsreihe mit den Einstellungen dieser Reihe. Möchtest du trotzdem eine weitere Aufgabe in dieser Vergleichsreihe testen?" + +-- Task {0}: chat 1 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T134264261"] = "Aufgabe {0}: Chat 1" + +-- Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T174840209"] = "Bearbeiten Sie Ihre Aufgabe mit diesem Modell wie in einem Chat. Sobald Sie fertig sind, erhält das andere Modell dieselbe erste Nachricht, und Sie arbeiten mit ihm weiter." + +-- Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T174840209::INFORMAL"] = "Bearbeite deine Aufgabe mit diesem Modell wie in einem Chat. Sobald du fertig bist, erhält das andere Modell dieselbe erste Nachricht, und du arbeitest mit ihm weiter." + +-- AI Studio could not save the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1811212186"] = "AI Studio konnte die Vergleichsreihe nicht speichern. Bitte versuchen Sie es erneut." + +-- AI Studio could not save the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1811212186::INFORMAL"] = "AI Studio konnte die Vergleichsreihe nicht speichern. Bitte versuche es erneut." + +-- How a task works +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2064032605"] = "So läuft eine Aufgabe ab" + +-- Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2374282918"] = "Beginnen Sie mit Ihrer ersten Aufgabe. Sie bearbeiten sie zunächst mit einem der beiden Modelle und anschließend mit dem anderen. AI Studio wählt zufällig aus, mit welchem Modell Sie zuerst arbeiten." + +-- Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2374282918::INFORMAL"] = "Beginne mit deiner ersten Aufgabe. Du bearbeitest sie zunächst mit einem der beiden Modelle und anschließend mit dem anderen. AI Studio wählt zufällig aus, mit welchem Modell du zuerst arbeitest." + +-- I'm done with this model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2855215253"] = "Ich bin mit diesem Modell fertig" + +-- Discard task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2966310514"] = "Aufgabe verwerfen" + +-- Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3114690087"] = "Möchten Sie diese Aufgabe und beide zugehörigen Chats wirklich verwerfen? Die Aufgabe wird nicht auf das Ergebnis angerechnet. Dies kann nicht rückgängig gemacht werden." + +-- Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3114690087::INFORMAL"] = "Möchtest du diese Aufgabe und beide zugehörigen Chats wirklich verwerfen? Die Aufgabe wird nicht auf das Ergebnis angerechnet. Dies kann nicht rückgängig gemacht werden." + +-- You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3203252119"] = "Sie können in dieser Reihe keine Bilder anhängen, da nicht beide Modelle sie lesen können. Andere Dateien, etwa Dokumente, funktionieren wie gewohnt." + +-- You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3203252119::INFORMAL"] = "Du kannst in dieser Reihe keine Bilder anhängen, da nicht beide Modelle sie lesen können. Andere Dateien, etwa Dokumente, funktionieren wie gewohnt." + +-- The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3217444240"] = "Das Ergebnis zeigt Ihnen, welches Modell in welchem Chat geantwortet hat. Aufgaben, die Sie danach entscheiden, gelten nicht mehr als blind: Das Ergebnis weist sie getrennt aus, und die Ergebnisse aller Vergleiche lassen sie weg. Möchten Sie das Ergebnis sehen?" + +-- The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3217444240::INFORMAL"] = "Das Ergebnis zeigt dir, welches Modell in welchem Chat geantwortet hat. Aufgaben, die du danach entscheidest, gelten nicht mehr als blind: Das Ergebnis weist sie getrennt aus, und die Ergebnisse aller Vergleiche lassen sie weg. Möchtest du das Ergebnis sehen?" + +-- Decided tasks so far: {0}. Do you want to test another task? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3225301908"] = "Bisher entschiedene Aufgaben: {0}. Möchten Sie eine weitere Aufgabe testen?" + +-- Decided tasks so far: {0}. Do you want to test another task? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3225301908::INFORMAL"] = "Bisher entschiedene Aufgaben: {0}. Möchtest du eine weitere Aufgabe testen?" + +-- Task {0}: your decision +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3586181851"] = "Aufgabe {0}: Ihre Entscheidung" + +-- Task {0}: your decision +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3586181851::INFORMAL"] = "Aufgabe {0}: deine Entscheidung" + +-- Start a task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3692047609"] = "Aufgabe starten" + +-- Setup of this series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3798429422"] = "Einstellungen dieser Vergleichsreihe" + +-- Hide the result +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T4133577695"] = "Ergebnis ausblenden" + +-- Task {0}: chat 2 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T83931404"] = "Aufgabe {0}: Chat 2" + +-- Complete answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T1705377513"] = "Vollständige Antworten" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T2189814010"] = "Modell" + +-- Until the first token +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T2818188291"] = "Bis zum ersten Token" + +-- The median time each model took, so that a single slow answer does not distort the picture. Only complete answers count. The times also depend on the length of the answers and on how busy the servers were. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T357780752"] = "Die mediane Antwortzeit jedes Modells, damit eine einzelne langsame Antwort das Gesamtbild nicht verzerrt. Es werden nur vollständige Antworten berücksichtigt. Die Zeiten hängen außerdem von der Länge der Antworten und der Auslastung der Server ab." + +-- Until the complete answer +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T517769867"] = "Bis zur vollständigen Antwort" + +-- A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T1958437853"] = "Ein Richter ist ein drittes Modell, das die Anfrage und die beiden Antworten jedes Durchlaufs liest und entscheidet, welche Antwort besser ist – genauso blind wie Sie. Er legt Ihre Erwartungen, Werte und Ziele an jeden Durchlauf gleich an und gibt Ihnen so eine zweite Meinung. Am Ende sehen Sie, wie oft der Richter Ihnen zugestimmt hat: Wenn das nur selten der Fall ist, sollten Sie Ihre Stimmen oder Ihre Beschreibung noch einmal überprüfen. Seine Urteile sehen Sie erst nach Ihren eigenen Stimmen, und er kostet eine zusätzliche Anfrage pro Durchlauf." + +-- A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T1958437853::INFORMAL"] = "Ein Richter ist ein drittes Modell, das die Anfrage und die beiden Antworten jedes Durchlaufs liest und entscheidet, welche Antwort besser ist – genauso blind wie du. Er legt deine Erwartungen, Werte und Ziele an jeden Durchlauf gleich an und gibt dir so eine zweite Meinung. Am Ende siehst du, wie oft der Richter dir zugestimmt hat: Wenn das nur selten der Fall ist, solltest du deine Stimmen oder deine Beschreibung noch einmal überprüfen. Seine Urteile siehst du erst nach deinen eigenen Stimmen, und er kostet eine zusätzliche Anfrage pro Durchlauf." + +-- Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2158946259"] = "Jedes Modell antwortet jedes Mal ein wenig anders, deshalb sagt ein einzelner Vergleich wenig aus. Bei mehreren Durchläufen stellt AI Studio beiden Modellen wiederholt dieselbe Anfrage, und Sie stimmen bei jedem Durchlauf ab, ohne zu wissen, welches Modell welche Antwort gegeben hat. Je mehr Durchläufe, desto aussagekräftiger das Ergebnis. Allerdings kostet jeder Durchlauf eine Anfrage pro Modell und mit Richter eine weitere." + +-- Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2158946259::INFORMAL"] = "Jedes Modell antwortet jedes Mal ein wenig anders, deshalb sagt ein einzelner Vergleich wenig aus. Bei mehreren Durchläufen stellt AI Studio beiden Modellen wiederholt dieselbe Anfrage, und du stimmst bei jedem Durchlauf ab, ohne zu wissen, welches Modell welche Antwort gegeben hat. Je mehr Durchläufe, desto aussagekräftiger das Ergebnis. Allerdings kostet jeder Durchlauf eine Anfrage pro Modell und mit Richter eine weitere." + +-- The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2347517054"] = "Das Material ist ein Text, mit dem beide Modelle arbeiten, z. B. ein Dokument, das zusammengefasst werden soll, eine E-Mail, auf die geantwortet werden soll, oder Code, der überprüft werden soll. Fügen Sie den Text ein oder laden Sie ihn aus einer Datei, z. B. einem PDF-Dokument, dessen Text AI Studio dann einliest. Lassen Sie das Feld leer, wenn Ihre Frage für sich allein steht." + +-- The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2347517054::INFORMAL"] = "Das Material ist ein Text, mit dem beide Modelle arbeiten, z. B. ein Dokument, das zusammengefasst werden soll, eine E-Mail, auf die geantwortet werden soll, oder Code, der überprüft werden soll. Füge den Text ein oder lade ihn aus einer Datei, z. B. einem PDF-Dokument, dessen Text AI Studio dann einliest. Lass das Feld leer, wenn deine Frage für sich allein steht." + +-- Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T3491184146"] = "Beide Modelle erhalten Ihre Frage oder Aufgabe und das untenstehende Material – und sonst nichts: keine Werkzeuge, Datenquellen, Postfächer oder Webseiten, auch dann nicht, wenn Sie diese im Chat verwenden. Eine Webadresse in Ihrer Anfrage bleibt reiner Text, den keines der Modelle öffnet. So zeigen die Antworten, was die Modelle selbst aus demselben Text machen. Wenn Sie vergleichen möchten, wie gut zwei Modelle Sie mit Werkzeugen, Datenquellen und Ihren eigenen Dateien unterstützen, nutzen Sie den Modus „Alltagsaufgaben“." + +-- Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T3491184146::INFORMAL"] = "Beide Modelle erhalten deine Frage oder Aufgabe und das untenstehende Material – und sonst nichts: keine Werkzeuge, Datenquellen, Postfächer oder Webseiten, auch dann nicht, wenn du diese im Chat verwendest. Eine Webadresse in deiner Anfrage bleibt reiner Text, den keines der Modelle öffnet. So zeigen die Antworten, was die Modelle selbst aus demselben Text machen. Wenn du vergleichen möchtest, wie gut zwei Modelle dich mit Werkzeugen, Datenquellen und deinen eigenen Dateien unterstützen, nutze den Modus „Alltagsaufgaben“." + +-- Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T477339319"] = "Wählen Sie die beiden Modelle aus, die Sie vergleichen möchten. Beide erhalten dieselbe Anfrage, und AI Studio zeigt ihre Antworten nebeneinander an – in zufälliger Reihenfolge und ohne ihre Namen. Sie stimmen blind darüber ab, welche Antwort besser ist, und erfahren erst am Ende, welches Modell welche Antwort verfasst hat. Sie können auch zweimal dasselbe Modell auswählen. Dann zeigt der Vergleich, wie stark seine Antworten variieren, wenn Sie ihm dieselbe Anfrage erneut stellen." + +-- Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T477339319::INFORMAL"] = "Wähle die beiden Modelle aus, die du vergleichen möchtest. Beide erhalten dieselbe Anfrage, und AI Studio zeigt ihre Antworten nebeneinander an – in zufälliger Reihenfolge und ohne ihre Namen. Du stimmst blind darüber ab, welche Antwort besser ist, und erfährst erst am Ende, welches Modell welche Antwort verfasst hat. Du kannst auch zweimal dasselbe Modell auswählen. Dann zeigt der Vergleich, wie stark seine Antworten variieren, wenn du ihm dieselbe Anfrage erneut stellst." + +-- Answer A +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T1420338340"] = "Antwort A" + +-- Answer B +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T1470671197"] = "Antwort B" + +-- Which answer is better? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2388547151"] = "Welche Antwort ist besser?" + +-- Finish +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2670325426"] = "Abschließen" + +-- Both are equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2723559248"] = "Beide sind gleich gut" + +-- Answer A is better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3123852190"] = "Antwort A ist besser" + +-- Skip remaining votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3188579167"] = "Verbleibende Stimmen überspringen" + +-- Skip this run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3334593867"] = "Diesen Durchlauf überspringen" + +-- Next run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3533713133"] = "Nächster Durchlauf" + +-- At least one of the models did not answer in this run, so there is nothing to compare. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3561467042"] = "Mindestens eines der Modelle hat in diesem Durchlauf nicht geantwortet. Daher gibt es nichts zu vergleichen." + +-- Run {0} of {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3815324322"] = "Durchlauf {0} von {1}" + +-- Answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T46578788"] = "Antworten" + +-- You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T651204000"] = "Sie wissen nicht, welches Modell welche Antwort verfasst hat, und die Spalten sind zufällig angeordnet. Vergleichen Sie die Antworten anhand ihres Inhalts. Die Namen erfahren Sie am Ende." + +-- You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T651204000::INFORMAL"] = "Du weißt nicht, welches Modell welche Antwort verfasst hat, und die Spalten sind zufällig angeordnet. Vergleiche die Antworten anhand ihres Inhalts. Die Namen erfährst du am Ende." + +-- Answer B is better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T933724747"] = "Antwort B ist besser" + -- You can enter text, attach one or more documents, or use both. At least one input is required. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "Sie können Text eingeben, ein oder mehrere Dokumente anhängen oder beides verwenden. Mindestens eine Eingabe ist erforderlich." @@ -6711,6 +7695,24 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T713722424"] = "AI Studio konnte d -- AI Studio could not store what the AI received separately from your chat. It remains in the chat itself, so you can still read it. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T713722424::INFORMAL"] = "AI Studio konnte die Inhalte, die die KI erhalten hat, nicht getrennt von deinem Chat speichern. Diese bleiben im Chat selbst, sodass du sie weiterhin lesen kannst." +-- The selected provider may not see this conversation. Please select another provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1132710328"] = "Der ausgewählte Anbieter darf diese Unterhaltung nicht sehen. Bitte wählen Sie einen anderen Anbieter aus." + +-- The selected provider may not see this conversation. Please select another provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1132710328::INFORMAL"] = "Der ausgewählte Anbieter darf diese Unterhaltung nicht sehen. Bitte wähle einen anderen Anbieter aus." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1809312323"] = "Bitte wählen Sie einen Anbieter aus." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1809312323::INFORMAL"] = "Bitte wähle einen Anbieter aus." + +-- AI Studio could not save this conversation. Please try again. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T463511884"] = "AI Studio konnte diese Unterhaltung nicht speichern. Bitte versuchen Sie es erneut." + +-- AI Studio could not save this conversation. Please try again. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T463511884::INFORMAL"] = "AI Studio konnte diese Unterhaltung nicht speichern. Bitte versuche es erneut." + -- The local image file does not exist. Skipping the image. UI_TEXT_CONTENT["AISTUDIO::CHAT::IMAGESOURCEEXTENSIONS::T255679918"] = "Die lokale Bilddatei existiert nicht. Das Bild wird übersprungen." @@ -7548,6 +8550,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T346886000"] = "Plugins Ihre -- Plugins of your organization. Your organization brings them back. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T346886000::INFORMAL"] = "Plugins deiner Organisation. Deine Organisation stellt sie wieder bereit." +-- Your presets, results, and comparison series with their chats in the Model Comparison assistant. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3574579770"] = "Ihre Voreinstellungen, Ergebnisse und Vergleichsreihen mit den zugehörigen Chats im Assistenten „Modellvergleich“." + +-- Your presets, results, and comparison series with their chats in the Model Comparison assistant. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3574579770::INFORMAL"] = "Deine Voreinstellungen, Ergebnisse und Vergleichsreihen mit den zugehörigen Chats im Assistenten „Modellvergleich“." + -- AI Studio keeps your data in two folders on this computer. When you back up your computer with a tool of your own, such as [Restic](https://restic.net), [Borg](https://www.borgbackup.org), or [Time Machine](https://support.apple.com/en-us/104984), this shows which of their contents belong in the backup and which it can leave out. Click a path to copy it to the clipboard, or use the folder button to open it in your file manager. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3596708716"] = "AI Studio speichert Ihre Daten in zwei Ordnern auf diesem Rechner. Wenn Sie Ihren Rechner mit einem eigenen Programm sichern, etwa mit [Restic](https://restic.net), [Borg](https://www.borgbackup.org) oder [Time Machine](https://support.apple.com/de-de/104984), sehen Sie hier, welche Inhalte dieser Ordner in die Sicherung gehören und welche sie auslassen kann. Klicken Sie auf einen Pfad, um ihn in die Zwischenablage zu kopieren, oder auf das Ordnersymbol, um ihn in Ihrem Dateimanager zu öffnen." @@ -7932,6 +8940,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T147584079"] = "Sie -- You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T147584079::INFORMAL"] = "Du hast die semantische Suche in den Werkzeugeinstellungen deaktiviert. Daher nutzt AI Studio das klassische RAG und durchsucht deine Datenquellen bei jeder Nachricht." +-- One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1491430062"] = "Eines der ausgewählten Modelle kann keine Werkzeuge verwenden. Daher nutzt AI Studio für alle Modelle das klassische RAG und durchsucht Ihre Datenquellen bei jeder Nachricht." + +-- One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1491430062::INFORMAL"] = "Eines der ausgewählten Modelle kann keine Werkzeuge verwenden. Daher nutzt AI Studio für alle Modelle das klassische RAG und durchsucht deine Datenquellen bei jeder Nachricht." + -- Data Source Selection UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T15302104"] = "Datenauswahl" @@ -7977,6 +8991,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2382076848"] = "Wen -- When the model of a chat cannot use tools, AI Studio uses classic RAG instead and searches your data sources with every message. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2382076848::INFORMAL"] = "Wenn das Modell eines Chats keine Werkzeuge verwenden kann, nutzt AI Studio das klassische RAG und durchsucht deine Datenquellen bei jeder Nachricht." +-- One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2654482616"] = "Einer der ausgewählten Anbieter ist für die semantische Suche nicht vertrauenswürdig genug. Daher nutzt AI Studio für alle das klassische RAG und durchsucht Ihre Datenquellen bei jeder Nachricht." + +-- One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2654482616::INFORMAL"] = "Einer der ausgewählten Anbieter ist für die semantische Suche nicht vertrauenswürdig genug. Daher nutzt AI Studio für alle das klassische RAG und durchsucht deine Datenquellen bei jeder Nachricht." + -- Whenever a question calls for it, the AI picks the fitting ones among these data sources itself. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2854302511"] = "Wann immer eine Frage es erfordert, wählt die KI selbst die passenden unter diesen Datenquellen aus." @@ -8139,6 +9159,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T244391 -- Change what this tile opens UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T4272203100"] = "Ändern, was diese Kachel öffnet" +-- Stop +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T1266644741"] = "Stoppen" + +-- AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T1561705002"] = "AI Studio kann diese Unterhaltung nicht öffnen. Die Datei ist möglicherweise beschädigt oder wurde mit einer neueren Version von AI Studio gespeichert." + +-- Export message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T274919936"] = "Nachricht exportieren" + +-- Your message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T3642105949"] = "Ihre Nachricht" + +-- Your message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T3642105949::INFORMAL"] = "Deine Nachricht" + +-- Send +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T4069397711"] = "Senden" + +-- approx. {0} tokens +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T857715435"] = "ca. {0} Tokens" + -- Copy the path: {0} UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEACTIONS::T1140628382"] = "Pfad kopieren: {0}" @@ -9834,6 +10875,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T749664565"] = "Werkzeugau -- No tool can be selected right now. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T953828816"] = "Derzeit kann kein Werkzeug ausgewählt werden." +-- This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T966234001"] = "Dieses Werkzeug erfordert das Vertrauensniveau {0}. Der ausgewählte Anbieter mit dem niedrigsten Vertrauensniveau hat {1}." + -- Select tools UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T998515990"] = "Werkzeuge auswählen" @@ -12570,6 +13614,240 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schl -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Schließen" +-- Compared +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1058933136"] = "Verglichen" + +-- Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1167627412"] = "Möchten Sie wirklich alle Ergebnisse des Modus „Antwortvarianz“ und alle Vergleichsreihen des Modus „Alltagsaufgaben“ samt Chats löschen, auch die, deren Ergebnis Sie noch nicht gesehen haben? Ihre Voreinstellungen bleiben erhalten. Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1167627412::INFORMAL"] = "Möchtest du wirklich alle Ergebnisse des Modus „Antwortvarianz“ und alle Vergleichsreihen des Modus „Alltagsaufgaben“ samt Chats löschen, auch die, deren Ergebnis du noch nicht gesehen hast? Deine Voreinstellungen bleiben erhalten. Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- {0}: {1} comparisons with {2} runs. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T119967148"] = "{0}: {1} Vergleiche mit {2} Durchläufen." + +-- All results +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1647689906"] = "Alle Ergebnisse" + +-- Speed +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T174817312"] = "Geschwindigkeit" + +-- Same choice +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1835176194"] = "Gleiche Wahl" + +-- Delete all comparison series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1848485659"] = "Alle Vergleichsreihen löschen" + +-- AI Studio could not delete all comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2092205828"] = "AI Studio konnte nicht alle Vergleichsreihen löschen. Bitte versuchen Sie es erneut." + +-- AI Studio could not delete all comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2092205828::INFORMAL"] = "AI Studio konnte nicht alle Vergleichsreihen löschen. Bitte versuche es erneut." + +-- Judge +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2094884932"] = "Richter" + +-- AI Studio could not delete all results. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2104729496"] = "AI Studio konnte nicht alle Ergebnisse löschen. Bitte versuchen Sie es erneut." + +-- AI Studio could not delete all results. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2104729496::INFORMAL"] = "AI Studio konnte nicht alle Ergebnisse löschen. Bitte versuche es erneut." + +-- No two different models were compared with each other yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2439811736"] = "Bisher wurden noch keine zwei unterschiedlichen Modelle miteinander verglichen." + +-- You have not voted on two different models yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2482334433"] = "Sie haben noch nicht über zwei verschiedene Modelle abgestimmt." + +-- You have not voted on two different models yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2482334433::INFORMAL"] = "Du hast noch nicht über zwei verschiedene Modelle abgestimmt." + +-- No judge has given a usable verdict yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T265789464"] = "Bisher hat noch kein Richter ein brauchbares Urteil abgegeben." + +-- How often each model won by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T268255753"] = "Wie oft jedes Modell nach den Urteilen der Richter gewonnen hat." + +-- Agreement +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2750704103"] = "Übereinstimmung" + +-- How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2888156258"] = "Wie oft jedes Modell nach Ihren Stimmen gewonnen hat. Ein Vergleich eines Modells mit sich selbst wird hier nicht mitgezählt, da er lediglich zeigt, wie konsistent das Modell antwortet." + +-- How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2888156258::INFORMAL"] = "Wie oft jedes Modell nach deinen Stimmen gewonnen hat. Ein Vergleich eines Modells mit sich selbst wird hier nicht mitgezählt, da er lediglich zeigt, wie konsistent das Modell antwortet." + +-- Ranking by the judges +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3071310071"] = "Rangliste nach den Richtern" + +-- Agreement with your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3243788701"] = "Übereinstimmung mit Ihren Stimmen" + +-- Agreement with your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3243788701::INFORMAL"] = "Übereinstimmung mit deinen Stimmen" + +-- Judges +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3347779221"] = "Richter" + +-- {0}: {1} comparison series with {2} tasks decided without knowing the models. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3355735368"] = "{0}: {1} Vergleichsreihen mit {2} Aufgaben, die ohne Kenntnis der Modelle entschieden wurden." + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3448155331"] = "Schließen" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T360017507"] = "Ihre Stimmen" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T360017507::INFORMAL"] = "Deine Stimmen" + +-- There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3704633531"] = "Es gibt noch keine Ergebnisse. Sie werden hier angezeigt, sobald Sie im Modus „Antwortvarianz“ über die Antworten abstimmen oder im Modus „Alltagsaufgaben“ das Ergebnis einer Vergleichsreihe ansehen." + +-- There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3704633531::INFORMAL"] = "Es gibt noch keine Ergebnisse. Sie werden hier angezeigt, sobald du im Modus „Antwortvarianz“ über die Antworten abstimmst oder im Modus „Alltagsaufgaben“ das Ergebnis einer Vergleichsreihe ansiehst." + +-- How often each of two models won when they competed directly, by your votes and by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T372027252"] = "Wie oft jedes der beiden Modelle im direkten Vergleich gewonnen hat – laut Ihren Stimmen und den Urteilen der Richter." + +-- How often each of two models won when they competed directly, by your votes and by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T372027252::INFORMAL"] = "Wie oft jedes der beiden Modelle im direkten Vergleich gewonnen hat – laut deinen Stimmen und den Urteilen der Richter." + +-- How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3831905913"] = "Wie oft jeder Richter dasselbe gewählt hat wie Sie. Es zählen nur Durchläufe und Aufgaben, für die sowohl Ihre Stimme als auch ein Urteil des Richters vorliegen." + +-- How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3831905913::INFORMAL"] = "Wie oft jeder Richter dasselbe gewählt hat wie du. Es zählen nur Durchläufe und Aufgaben, für die sowohl deine Stimme als auch ein Urteil des Richters vorliegen." + +-- Delete all results and comparison series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3967608788"] = "Alle Ergebnisse und Vergleichsreihen löschen" + +-- Delete the results of “Answer Variance” +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T4059958648"] = "Ergebnisse der „Antwortvarianz“ löschen" + +-- Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T726138079"] = "Möchten Sie wirklich alle Ergebnisse des Modus „Antwortvarianz“ löschen? Ihre Voreinstellungen und die Vergleichsreihen des Modus „Alltagsaufgaben“ bleiben erhalten. Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T726138079::INFORMAL"] = "Möchtest du wirklich alle Ergebnisse des Modus „Antwortvarianz“ löschen? Deine Voreinstellungen und die Vergleichsreihen des Modus „Alltagsaufgaben“ bleiben erhalten. Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Pairs +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T865427974"] = "Paare" + +-- Only the mode “Answer Variance” measures how fast the models answer. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T922668471"] = "Nur der Modus „Antwortvarianz“ misst, wie schnell die Modelle antworten." + +-- Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T93821586"] = "Möchten Sie wirklich alle Vergleichsreihen des Modus „Alltagsaufgaben“ samt Chats löschen, auch die, deren Ergebnis Sie noch nicht gesehen haben? Ihre Voreinstellungen und die Ergebnisse des Modus „Antwortvarianz“ bleiben erhalten. Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T93821586::INFORMAL"] = "Möchtest du wirklich alle Vergleichsreihen des Modus „Alltagsaufgaben“ samt Chats löschen, auch die, deren Ergebnis du noch nicht gesehen hast? Deine Voreinstellungen und die Ergebnisse des Modus „Antwortvarianz“ bleiben erhalten. Dieser Vorgang kann nicht rückgängig gemacht werden." + +-- Judge (optional) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1206060603"] = "Richter (optional)" + +-- This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T134968648"] = "Diese Vergleichsreihe beginnt mit den Einstellungen der Vergleichsreihe „{0}“. Sie können alles davon ändern, bevor Sie die Vergleichsreihe erstellen. Die Aufgaben bleiben in ihrer Vergleichsreihe." + +-- This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T134968648::INFORMAL"] = "Diese Vergleichsreihe beginnt mit den Einstellungen der Vergleichsreihe „{0}“. Du kannst alles davon ändern, bevor du die Vergleichsreihe erstellst. Die Aufgaben bleiben in ihrer Vergleichsreihe." + +-- Please choose two different models. A series compares how well two models help you. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1531478547"] = "Bitte wählen Sie zwei unterschiedliche Modelle aus. Eine Vergleichsreihe vergleicht, wie gut Ihnen zwei Modelle helfen." + +-- Please choose two different models. A series compares how well two models help you. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1531478547::INFORMAL"] = "Bitte wähle zwei unterschiedliche Modelle aus. Eine Vergleichsreihe vergleicht, wie gut dir zwei Modelle helfen." + +-- Create series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2037222750"] = "Vergleichsreihe erstellen" + +-- Name of the series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2083243781"] = "Name der Vergleichsreihe" + +-- No judge +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2562272449"] = "Kein Richter" + +-- Name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T266367750"] = "Name" + +-- The profile no longer exists, so the series starts without a profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2782528640"] = "Das Profil existiert nicht mehr, daher beginnt die Vergleichsreihe ohne Profil." + +-- The profile no longer exists, so the series starts without a profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2782528640::INFORMAL"] = "Das Profil existiert nicht mehr, daher beginnt die Vergleichsreihe ohne Profil." + +-- Models +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T30216107"] = "Modelle" + +-- The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3472943722"] = "Das zweite Modell ist nicht mehr verfügbar, verwendet jetzt ein anderes Modell oder darf von diesem Assistenten nicht mehr verwendet werden. Bitte wählen Sie es erneut aus." + +-- The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3472943722::INFORMAL"] = "Das zweite Modell ist nicht mehr verfügbar, verwendet jetzt ein anderes Modell oder darf von diesem Assistenten nicht mehr verwendet werden. Bitte wähle es erneut aus." + +-- Ask a judge? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3641185871"] = "Einen Richter fragen?" + +-- Tools: {0}, data sources: {1} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3667830435"] = "Werkzeuge: {0}, Datenquellen: {1}" + +-- The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3712631176"] = "Das erste Modell ist nicht mehr verfügbar, verwendet jetzt ein anderes Modell oder darf von diesem Assistenten nicht mehr verwendet werden. Bitte wählen Sie es erneut aus." + +-- The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3712631176::INFORMAL"] = "Das erste Modell ist nicht mehr verfügbar, verwendet jetzt ein anderes Modell oder darf von diesem Assistenten nicht mehr verwendet werden. Bitte wähle es erneut aus." + +-- AI Studio could not save the new comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3741421828"] = "AI Studio konnte die neue Vergleichsreihe nicht speichern. Bitte versuchen Sie es erneut." + +-- AI Studio could not save the new comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3741421828::INFORMAL"] = "AI Studio konnte die neue Vergleichsreihe nicht speichern. Bitte versuche es erneut." + +-- Profile, tools, and data sources +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3936393556"] = "Profil, Werkzeuge und Datenquellen" + +-- Choose both models first: what you can select here depends on them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4030857378"] = "Wählen Sie zuerst beide Modelle aus. Welche Auswahlmöglichkeiten Ihnen hier zur Verfügung stehen, hängt von den Modellen ab." + +-- Choose both models first: what you can select here depends on them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4030857378::INFORMAL"] = "Wähle zuerst beide Modelle aus. Welche Auswahlmöglichkeiten dir hier zur Verfügung stehen, hängt von den Modellen ab." + +-- First model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4044223184"] = "Erstes Modell" + +-- A judge assesses every task +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4111877217"] = "Ein Richter beurteilt jede Aufgabe" + +-- Some data sources are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T511469979"] = "Einige Datenquellen sind nicht mehr verfügbar, daher beginnt die Vergleichsreihe ohne sie." + +-- Some data sources are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T511469979::INFORMAL"] = "Einige Datenquellen sind nicht mehr verfügbar, daher beginnt die Vergleichsreihe ohne sie." + +-- Some tools are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T600304160"] = "Einige Werkzeuge sind nicht mehr verfügbar, daher beginnt die Vergleichsreihe ohne sie." + +-- Some tools are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T600304160::INFORMAL"] = "Einige Werkzeuge sind nicht mehr verfügbar, daher beginnt die Vergleichsreihe ohne sie." + +-- e.g., Weekly reports +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T795365587"] = "z. B. wöchentliche Berichte" + +-- The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T805855292"] = "Das Richtermodell ist nicht mehr verfügbar, verwendet jetzt ein anderes Modell oder darf von diesem Assistenten nicht mehr verwendet werden. Bitte wählen Sie ein anderes aus, oder schalten Sie den Richter aus." + +-- The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T805855292::INFORMAL"] = "Das Richtermodell ist nicht mehr verfügbar, verwendet jetzt ein anderes Modell oder darf von diesem Assistenten nicht mehr verwendet werden. Bitte wähle ein anderes aus, oder schalte den Richter aus." + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T867467332"] = "Zweites Modell" + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T900713019"] = "Abbrechen" + +-- Judge model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T953975257"] = "Richtermodell" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems." @@ -14577,6 +15855,66 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4049873 -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Den Web-Content-Reader vorauswählen?" +-- No preset preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1028082569"] = "Keine Voreinstellung vorausgewählt" + +-- Preselected preset +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1094542630"] = "Vorausgewählte Voreinstellung" + +-- The mode “Answer Variance” opens with this preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1310335465"] = "Der Modus „Antwortvarianz“ wird mit dieser Voreinstellung geöffnet." + +-- No Model Comparison options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1495085044"] = "Es sind keine Optionen für den Modellvergleich vorausgewählt" + +-- Model Comparison options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2330752759"] = "Optionen für den Modellvergleich sind vorausgewählt" + +-- Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2350726718"] = "Dieses Modell wird bei jeder neuen Voreinstellung im Modus „Antwortvarianz“ und bei jeder neuen Vergleichsreihe im Modus „Alltagsaufgaben“ als erstes Modell verwendet." + +-- Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2350726718::INFORMAL"] = "Dieses Modell wird bei jeder neuen Voreinstellung im Modus „Antwortvarianz“ und bei jeder neuen Vergleichsreihe im Modus „Alltagsaufgaben“ als erstes Modell verwendet." + +-- The preselected preset no longer exists. Please select another preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2696863075"] = "Die vorausgewählte Voreinstellung existiert nicht mehr. Bitte wählen Sie eine andere Voreinstellung." + +-- The preselected preset no longer exists. Please select another preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2696863075::INFORMAL"] = "Die vorausgewählte Voreinstellung existiert nicht mehr. Bitte wähle eine andere Voreinstellung." + +-- No model preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2802610587"] = "Kein Modell vorausgewählt" + +-- When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2982794040"] = "Wenn diese Option aktiviert ist, werden für neue Voreinstellungen und Vergleichsreihen zunächst die hier vorausgewählten Modelle verwendet, z. B. das Modell, mit dem Sie täglich arbeiten und mit dem Sie neue Modelle vergleichen möchten." + +-- When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2982794040::INFORMAL"] = "Wenn diese Option aktiviert ist, werden für neue Voreinstellungen und Vergleichsreihen zunächst die hier vorausgewählten Modelle verwendet, z. B. das Modell, mit dem du täglich arbeitest und mit dem du neue Modelle vergleichen möchtest." + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3448155331"] = "Schließen" + +-- Preselect Model Comparison options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3640087463"] = "Optionen für den Modellvergleich vorauswählen?" + +-- Preselected judge model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3746033265"] = "Vorausgewähltes Richtermodell" + +-- A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3884208586"] = "Ein vorausgewähltes Richtermodell schaltet den Richter in jeder neuen Voreinstellung und jeder neuen Vergleichsreihe ein. Ohne eines bleibt der Richter aus, bis Sie ihn einschalten." + +-- A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3884208586::INFORMAL"] = "Ein vorausgewähltes Richtermodell schaltet den Richter in jeder neuen Voreinstellung und jeder neuen Vergleichsreihe ein. Ohne eines bleibt der Richter aus, bis du ihn einschaltest." + +-- Missing preset ({0}) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T413828499"] = "Fehlende Voreinstellung ({0})" + +-- Assistant: Model Comparison Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T4212945869"] = "Assistent: Optionen für den Modellvergleich" + +-- Preselected model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T473925378"] = "Vorausgewähltes Modell" + -- Choose whether the assistant should use the app default profile, no profile, or a specific profile. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Wählen Sie, ob der Assistent das Standardprofil der App, kein Profil oder ein bestimmtes Profil verwenden soll." @@ -15969,6 +17307,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1777666968"] = "Prompt-Optimierer -- SWOT Analysis UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1864542518"] = "SWOT-Analyse" +-- Compare two models in a blind test for how much their answers vary and how useful they are. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1869142375"] = "Vergleichen Sie zwei Modelle im Blindtest hinsichtlich der Varianz ihrer Antworten und ihrer Nützlichkeit." + +-- Compare two models in a blind test for how much their answers vary and how useful they are. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1869142375::INFORMAL"] = "Vergleiche zwei Modelle im Blindtest hinsichtlich der Varianz ihrer Antworten und ihrer Nützlichkeit." + -- Slide Assistant UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Folien-Assistent" @@ -16083,6 +17427,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3881504200"] = "Verwenden Sie ein -- Use an LLM to find an icon for a given context. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3881504200::INFORMAL"] = "Verwende ein LLM, um ein Icon für einen bestimmten Kontext zu finden." +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3910956815"] = "Modellvergleich" + -- Job Posting UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3930052338"] = "Stellenanzeige" @@ -17787,6 +19134,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "Ans -- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511::INFORMAL"] = "Anscheinend hast du bei OpenAI kein API-Guthaben mehr. Bitte füge deinem Konto Guthaben hinzu und versuche es erneut." +-- hidden +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::PROVIDERIDENTITYEXTENSIONS::T4128829753"] = "verborgen" + -- Text too long UI_TEXT_CONTENT["AISTUDIO::PROVIDER::PROVIDERREQUESTFAILUREREASONEXTENSIONS::T1074711534"] = "Text zu lang" @@ -18480,6 +19830,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T555062689"] = "Assisten -- New Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "Neuer Chat" +-- Model Comparison Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T941908687"] = "Assistent zum Modellvergleich" + -- Trust LLM providers from the USA UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T1748300640"] = "LLM-Anbietern aus den USA vertrauen" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 4f5212c5a..7d50da6ab 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -4617,6 +4617,990 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133 -- Clear UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T77955010"] = "Clear" +-- Material: {0} characters +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1097973848"] = "Material: {0} characters" + +-- Judge (optional) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1206060603"] = "Judge (optional)" + +-- Give your preset a name which tells you later what it compares. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1326490469"] = "Give your preset a name which tells you later what it compares." + +-- Give your preset a name which tells you later what it compares. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1326490469::INFORMAL"] = "Give your preset a name which tells you later what it compares." + +-- Please provide a name for your preset. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1408294373"] = "Please provide a name for your preset." + +-- Please provide a name for your preset. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1408294373::INFORMAL"] = "Please provide a name for your preset." + +-- 20 (heavy load on the models) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1426001529"] = "20 (heavy load on the models)" + +-- Please provide a question or task for both models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1481551697"] = "Please provide a question or task for both models." + +-- Please provide a question or task for both models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1481551697::INFORMAL"] = "Please provide a question or task for both models." + +-- Delete this preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T148310729"] = "Delete this preset" + +-- Your request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1607440787"] = "Your request" + +-- Your request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1607440787::INFORMAL"] = "Your request" + +-- Your question or task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1646098156"] = "Your question or task" + +-- Your question or task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1646098156::INFORMAL"] = "Your question or task" + +-- Material (optional) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1771342343"] = "Material (optional)" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T1990305431"] = "Results of all comparisons" + +-- Switch mode +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2045889708"] = "Switch mode" + +-- Add preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2059304803"] = "Add preset" + +-- 1 (single comparison) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2267053576"] = "1 (single comparison)" + +-- No judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2562272449"] = "No judge" + +-- AI Studio could not save the results of this comparison for the statistics. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2762349476"] = "AI Studio could not save the results of this comparison for the statistics." + +-- Preset from {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T2841002060"] = "Preset from {0}" + +-- Models +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T30216107"] = "Models" + +-- The name of your preset must be between 6 and 60 characters long. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3065222761"] = "The name of your preset must be between 6 and 60 characters long." + +-- The name of your preset must be between 6 and 60 characters long. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3065222761::INFORMAL"] = "The name of your preset must be between 6 and 60 characters long." + +-- A judge assesses every run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3109664129"] = "A judge assesses every run" + +-- Are you sure you want to delete the preset '{0}'? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3246395362"] = "Are you sure you want to delete the preset '{0}'? This cannot be undone." + +-- Are you sure you want to delete the preset '{0}'? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3246395362::INFORMAL"] = "Are you sure you want to delete the preset '{0}'? This cannot be undone." + +-- Delete preset +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3276319725"] = "Delete preset" + +-- Material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3419754368"] = "Material" + +-- Compare +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3581545044"] = "Compare" + +-- Ask a judge? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3641185871"] = "Ask a judge?" + +-- Load material from a file +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3699307713"] = "Load material from a file" + +-- Expand +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3889190145"] = "Expand" + +-- Presets +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T3897629751"] = "Presets" + +-- Preset name +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T40334785"] = "Preset name" + +-- A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4043921725"] = "A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself." + +-- A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4043921725::INFORMAL"] = "A preset keeps everything you set up below, so that you can run the same comparison again later, e.g., once a new model is available. You always work in a preset, and AI Studio saves your changes by itself." + +-- First model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4044223184"] = "First model" + +-- AI Studio could not save the change to your presets. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4152209182"] = "AI Studio could not save the change to your presets. Please try again." + +-- AI Studio could not save the change to your presets. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T4152209182::INFORMAL"] = "AI Studio could not save the change to your presets. Please try again." + +-- Collapse +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T5885182"] = "Collapse" + +-- A preset with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T599249526"] = "A preset with this name already exists. Please choose a different name." + +-- A preset with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T599249526::INFORMAL"] = "A preset with this name already exists. Please choose a different name." + +-- Number of runs +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T752176293"] = "Number of runs" + +-- Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T764471416"] = "Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer." + +-- Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T764471416::INFORMAL"] = "Send the same request to two models, once or many times, and compare their answers side by side. You vote blindly for the better answer, see how much the answers vary, and learn at the end which model wrote which answer." + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T867467332"] = "Second model" + +-- Judge model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISON::T953975257"] = "Judge model" + +-- Enter a new name for this comparison series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1211403772"] = "Enter a new name for this comparison series." + +-- Enter a new name for this comparison series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1211403772::INFORMAL"] = "Enter a new name for this comparison series." + +-- Open +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1401622761"] = "Open" + +-- Start a new series with this setup +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1452503898"] = "Start a new series with this setup" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T1990305431"] = "Results of all comparisons" + +-- Delete series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2022370503"] = "Delete series" + +-- Switch mode +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2045889708"] = "Switch mode" + +-- Name of the series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2083243781"] = "Name of the series" + +-- Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2398358753"] = "Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone." + +-- Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2398358753::INFORMAL"] = "Are you sure you want to delete the comparison series '{0}' with all its chats? This cannot be undone." + +-- Your comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T242544328"] = "Your comparison series" + +-- Your comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T242544328::INFORMAL"] = "Your comparison series" + +-- AI Studio could not rename the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2698444055"] = "AI Studio could not rename the comparison series. Please try again." + +-- AI Studio could not rename the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2698444055::INFORMAL"] = "AI Studio could not rename the comparison series. Please try again." + +-- Back to all series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2733157589"] = "Back to all series" + +-- Started {0}, decided tasks: {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2771954403"] = "Started {0}, decided tasks: {1}" + +-- Started {0}, decided tasks: {1}, result shown +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T2970598131"] = "Started {0}, decided tasks: {1}, result shown" + +-- Rename +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3355849203"] = "Rename" + +-- Rename series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3550827604"] = "Rename series" + +-- AI Studio could not delete the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3650237868"] = "AI Studio could not delete the comparison series. Please try again." + +-- AI Studio could not delete the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3650237868::INFORMAL"] = "AI Studio could not delete the comparison series. Please try again." + +-- You have no comparison series yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T369101477"] = "You have no comparison series yet." + +-- You have no comparison series yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T369101477::INFORMAL"] = "You have no comparison series yet." + +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T3910956815"] = "Model Comparison" + +-- New comparison series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T4249192191"] = "New comparison series" + +-- Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T922224005"] = "Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one." + +-- Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::ASSISTANTMODELCOMPARISONTASKS::T922224005::INFORMAL"] = "Open a series to work on another task or to look at its result. The list does not name the models, so that you do not learn which one is which before you look at the result. To compare the same models again, or to replace one of them with a new model, start a new series with the setup of an existing one." + +-- First token after {0}, complete after {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCAPTION::T1658271318"] = "First token after {0}, complete after {1}" + +-- Judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCAPTION::T2094884932"] = "Judge" + +-- This model did not answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONANSWERCARD::T472607376"] = "This model did not answer." + +-- All {0} runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1508408704"] = "All {0} runs are done." + +-- Start a new request +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1767930003"] = "Start a new request" + +-- Judge verdicts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1879874010"] = "Judge verdicts" + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T1990305431"] = "Results of all comparisons" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T248601509"] = "Equally good" + +-- {0} of {1} runs have no usable judge verdict. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2716994296"] = "{0} of {1} runs have no usable judge verdict. They are not counted above." + +-- {0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2766679987"] = "{0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above." + +-- {0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T2766679987::INFORMAL"] = "{0} of {1} runs have no vote from you, because you skipped them or a model did not answer. They are not counted above." + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T360017507"] = "Your votes" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T360017507::INFORMAL"] = "Your votes" + +-- Done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHSUMMARY::T38691245"] = "Done." + +-- Answer A +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1420338340"] = "Answer A" + +-- Answer B +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1470671197"] = "Answer B" + +-- You skipped the remaining votes. The summary follows once the remaining runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1964601229"] = "You skipped the remaining votes. The summary follows once the remaining runs are done." + +-- You skipped the remaining votes. The summary follows once the remaining runs are done. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T1964601229::INFORMAL"] = "You skipped the remaining votes. The summary follows once the remaining runs are done." + +-- Both models are answering. This can take a while. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T2709682697"] = "Both models are answering. This can take a while." + +-- Waiting for the next run... +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONBATCHVIEW::T758625750"] = "Waiting for the next run..." + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T1422468824"] = "Your messages on average" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T1422468824::INFORMAL"] = "Your messages on average" + +-- Median time +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONEFFORTCHARTS::T2152829620"] = "Median time" + +-- Describe your expectations, values, and goals to the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T1734361278"] = "Describe your expectations, values, and goals to the judge" + +-- Describe your expectations, values, and goals to the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T1734361278::INFORMAL"] = "Describe your expectations, values, and goals to the judge" + +-- Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2577739352"] = "Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right." + +-- Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2577739352::INFORMAL"] = "Please describe your expectations, values, and goals to the judge, or confirm that an AI may decide for you what is good and right." + +-- Your expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2641486288"] = "Your expectations, values, and goals" + +-- Your expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2641486288::INFORMAL"] = "Your expectations, values, and goals" + +-- Yes, an AI may decide for me what is good and right +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T2805496671"] = "Yes, an AI may decide for me what is good and right" + +-- For example: I prefer short answers which get to the point. Technical terms are welcome, since I work in the field. An answer should weigh several views instead of taking a side. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T326873300"] = "For example: I prefer short answers which get to the point. Technical terms are welcome, since I work in the field. An answer should weigh several views instead of taking a side." + +-- Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T332317276"] = "Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do." + +-- Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T332317276::INFORMAL"] = "Without your expectations, values, and goals, the judge decides by its own standards: by what it learned in training, with the bias of those who trained it. Its verdicts then tell you what this AI considers good and right, not what you do." + +-- I confirm that I have read and understood the above +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T3683380716"] = "I confirm that I have read and understood the above" + +-- No, I will describe my expectations, values, and goals +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T401061652"] = "No, I will describe my expectations, values, and goals" + +-- What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T588888121"] = "What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models." + +-- What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEEXPECTATIONS::T588888121::INFORMAL"] = "What makes an answer good is up to you, not up to an AI. Without your guidance, the judge decides by what it learned in training: by the values, preferences, and bias of those who trained it, e.g., of a provider in the US or in China. Is a long, detailed answer better than a short one? Should an answer name its sources? Should it take a side or weigh several views? Only you can say. Therefore, describe to the judge as precisely as you can what you expect, what matters to you, and what you want to achieve. We strongly recommend it: otherwise, the verdicts say more about the judge than about your models." + +-- The judge saw both answers as equally good, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T110452334"] = "The judge saw both answers as equally good, matching your own vote." + +-- The judge saw both answers as equally good, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T110452334::INFORMAL"] = "The judge saw both answers as equally good, matching your own vote." + +-- The judge preferred {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1418012290"] = "The judge preferred {0}." + +-- The judge preferred {0}, differing from your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1499258629"] = "The judge preferred {0}, differing from your own vote." + +-- The judge preferred {0}, differing from your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1499258629::INFORMAL"] = "The judge preferred {0}, differing from your own vote." + +-- The judge saw both answers as equally good. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T1679196260"] = "The judge saw both answers as equally good." + +-- The judge preferred {0}, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T2457939612"] = "The judge preferred {0}, matching your own vote." + +-- The judge preferred {0}, matching your own vote. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T2457939612::INFORMAL"] = "The judge preferred {0}, matching your own vote." + +-- The judge saw both answers as equally good, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T3189061594"] = "The judge saw both answers as equally good, while you preferred one of them." + +-- The judge saw both answers as equally good, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T3189061594::INFORMAL"] = "The judge saw both answers as equally good, while you preferred one of them." + +-- Judge: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T4194679568"] = "Judge: {0}" + +-- The judge did not give a usable verdict. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONJUDGEVERDICTCARD::T4278818434"] = "The judge did not give a usable verdict." + +-- Compare two models without knowing which one answers. Choose what you want to find out. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1060503021"] = "Compare two models without knowing which one answers. Choose what you want to find out." + +-- Compare two models without knowing which one answers. Choose what you want to find out. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1060503021::INFORMAL"] = "Compare two models without knowing which one answers. Choose what you want to find out." + +-- Results of all comparisons +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T1990305431"] = "Results of all comparisons" + +-- Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T2256226870"] = "Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer." + +-- Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T2256226870::INFORMAL"] = "Send the same request to both models, once or many times in a row, and vote blindly for the better answer each time. You learn which model answers better, and how much its answers vary from run to run. An optional judge model gives its own verdict. At the end, you see which model wrote which answer." + +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T3910956815"] = "Model Comparison" + +-- Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T4217745718"] = "Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks." + +-- Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODECHOICE::T4217745718::INFORMAL"] = "Work on a real task of your everyday work with one of the two models, with follow-up questions, tools, and data sources, and then on the same task with the other one. Afterward, you decide which chat helped you more, without knowing which model was which. Each task adds to the result, over days or weeks." + +-- Model Comparison: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T2204151393"] = "Model Comparison: {0}" + +-- Everyday Tasks +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T2633045292"] = "Everyday Tasks" + +-- Answer Variance +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONMODEEXTENSIONS::T3575446502"] = "Answer Variance" + +-- A series with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T1933751712"] = "A series with this name already exists. Please choose a different name." + +-- A series with this name already exists. Please choose a different name. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T1933751712::INFORMAL"] = "A series with this name already exists. Please choose a different name." + +-- Please give the series a name, so that you find it again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T2512778872"] = "Please give the series a name, so that you find it again." + +-- Please give the series a name, so that you find it again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T2512778872::INFORMAL"] = "Please give the series a name, so that you find it again." + +-- The name of your series may have at most {0} characters. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T4001587725"] = "The name of your series may have at most {0} characters." + +-- The name of your series may have at most {0} characters. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONNAMES::T4001587725::INFORMAL"] = "The name of your series may have at most {0} characters." + +-- {0} is clearly better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1539751573"] = "{0} is clearly better" + +-- Stored comparisons: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1864825956"] = "Stored comparisons: {0}" + +-- Judge verdicts +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T1879874010"] = "Judge verdicts" + +-- Assessment +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T2135313999"] = "Assessment" + +-- {0} vs. {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T2197341449"] = "{0} vs. {1}" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T248601509"] = "Equally good" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T360017507"] = "Your votes" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T360017507::INFORMAL"] = "Your votes" + +-- Not yet clear +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPAIRCARD::T877424713"] = "Not yet clear" + +-- The use of this provider has been prohibited by your organization. Please select a different provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1807387355"] = "The use of this provider has been prohibited by your organization. Please select a different provider." + +-- The use of this provider has been prohibited by your organization. Please select a different provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1807387355::INFORMAL"] = "The use of this provider has been prohibited by your organization. Please select a different provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1809312323"] = "Please select a provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T1809312323::INFORMAL"] = "Please select a provider." + +-- This provider does not meet the confidence requirements of this assistant. Please choose another one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T512106357"] = "This provider does not meet the confidence requirements of this assistant. Please choose another one." + +-- This provider does not meet the confidence requirements of this assistant. Please choose another one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONPROVIDERCHECK::T512106357::INFORMAL"] = "This provider does not meet the confidence requirements of this assistant. Please choose another one." + +-- Clearly worse +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T1738164753"] = "Clearly worse" + +-- Assessment +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2135313999"] = "Assessment" + +-- Wins +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2168528702"] = "Wins" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2189814010"] = "Model" + +-- {0} to {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T2854601881"] = "{0} to {1}" + +-- Losses +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3417493210"] = "Losses" + +-- Clearly better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3673745789"] = "Clearly better" + +-- Ties +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3724044438"] = "Ties" + +-- Share of wins +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T3818681848"] = "Share of wins" + +-- Likely range +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T554551492"] = "Likely range" + +-- Not yet clear +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRANKING::T877424713"] = "Not yet clear" + +-- “Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T1602691513"] = "“Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself." + +-- “Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T1602691513::INFORMAL"] = "“Compared” counts the runs and tasks with both your vote and a verdict of the judge, “Same choice” those in which the judge chose the same as you, and “Agreement” their share. The higher it is, the more the judge decides like you, and the more its verdicts tell you where you did not vote yourself." + +-- The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2051424113"] = "The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner." + +-- The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2051424113::INFORMAL"] = "The effort tells you how much each model asked of you until you were done with a task: how many messages you wrote on average, your first one included, and how long you worked with the model. For the time, AI Studio takes the median: half of the tasks took less, half took more, so that a chat left open overnight does not distort the picture. Fewer messages and less time can mean that a model understood you sooner." + +-- The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2144465448"] = "The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you." + +-- The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2144465448::INFORMAL"] = "The result counts only the tasks you decided without knowing the models. It shows how often each model helped you more, whether that is clearly more than chance, and how much effort each model cost you." + +-- The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2456227034"] = "The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share." + +-- The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T2456227034::INFORMAL"] = "The columns follow the order in the heading: the first model, both equally good, the second model. The assessment says whether one of the two is clearly better. That is the case only when the likely range of its share of wins, at a confidence of 95 percent, lies entirely above one half; the ties do not count toward the share." + +-- The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T3079420033"] = "The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too." + +-- The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T3079420033::INFORMAL"] = "The share of wins counts only the runs and tasks which one of the models won, not the ties. With few of them, chance plays a large part. Therefore, a model counts as clearly better or clearly worse only when the likely range of its share, at a confidence of 95 percent, lies entirely above or below one half. Many runs of the same request tell you a lot about that request, but little about others, so compare the models on different requests and tasks, too." + +-- Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T584847906"] = "Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together." + +-- Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T584847906::INFORMAL"] = "Here you see how your votes and the verdicts of the judge turned out for this request. A single request tells you little about how the models do in general: compare them on several requests, and on real tasks in the mode “Everyday Tasks”, before you choose one. The results of all comparisons put everything together." + +-- Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T770148061"] = "Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them." + +-- Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRESULTTEXTS::T770148061::INFORMAL"] = "Here you see what all your comparisons say about the models. The mode “Answer Variance” adds your votes on the answers of two models to the same request, and the mode “Everyday Tasks” adds your decisions on real tasks. A comparison series counts only once you looked at its result, and only with the tasks you decided before, while you did not know the models yet. Choose above whether the results of both modes count, or those of one of them." + +-- Run {0}: without your vote +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1440259155"] = "Run {0}: without your vote" + +-- Run {0}: without your vote +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1440259155::INFORMAL"] = "Run {0}: without your vote" + +-- Run {0}: you found both answers equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1728842697"] = "Run {0}: you found both answers equally good" + +-- Run {0}: you found both answers equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1728842697::INFORMAL"] = "Run {0}: you found both answers equally good" + +-- Run {0}: at least one model did not answer +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T1775562627"] = "Run {0}: at least one model did not answer" + +-- Equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T248601509"] = "Equally good" + +-- First model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4044223184"] = "First model" + +-- Run {0}: you preferred {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4293630979"] = "Run {0}: you preferred {1}" + +-- Run {0}: you preferred {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T4293630979::INFORMAL"] = "Run {0}: you preferred {1}" + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPANEL::T867467332"] = "Second model" + +-- Run {0}: the judge gave no usable verdict, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1221245882"] = "Run {0}: the judge gave no usable verdict, your vote is missing." + +-- Run {0}: the judge gave no usable verdict, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1221245882::INFORMAL"] = "Run {0}: the judge gave no usable verdict, your vote is missing." + +-- Run {0}: no usable answer, nothing to vote on. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1407798402"] = "Run {0}: no usable answer, nothing to vote on." + +-- Run {0}: queued, waiting for a free slot. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1600973378"] = "Run {0}: queued, waiting for a free slot." + +-- Run {0}: still working. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T1838060603"] = "Run {0}: still working." + +-- Run {0}: the models and the judge are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2115467037"] = "Run {0}: the models and the judge are done, your vote is missing." + +-- Run {0}: the models and the judge are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2115467037::INFORMAL"] = "Run {0}: the models and the judge are done, your vote is missing." + +-- Progress of the runs +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2330056762"] = "Progress of the runs" + +-- Run {0}: your vote was skipped. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2715329048"] = "Run {0}: your vote was skipped." + +-- Run {0}: your vote was skipped. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T2715329048::INFORMAL"] = "Run {0}: your vote was skipped." + +-- Run {0}: voted on. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T3789682551"] = "Run {0}: voted on." + +-- Run {0}: the models are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T4197671504"] = "Run {0}: the models are done, your vote is missing." + +-- Run {0}: the models are done, your vote is missing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONRUNPROGRESS::T4197671504::INFORMAL"] = "Run {0}: the models are done, your vote is missing." + +-- Chat 1 helped more +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2438409751"] = "Chat 1 helped more" + +-- Chat 1 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2553260352"] = "Chat 1" + +-- Chat 2 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T2603593209"] = "Chat 2" + +-- Both helped equally +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3093762271"] = "Both helped equally" + +-- Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3870350915"] = "Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat." + +-- Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T3870350915::INFORMAL"] = "Which chat helped you more with your task? Read both chats again if you like. You still do not know which model answered in which chat." + +-- Chat 2 helped more +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESDECISION::T988417422"] = "Chat 2 helped more" + +-- Tasks decided without knowing the models: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1092985245"] = "Tasks decided without knowing the models: {0}" + +-- The judge chose the same as you in {0} of {1} tasks ({2}). +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T118785941"] = "The judge chose the same as you in {0} of {1} tasks ({2})." + +-- The judge chose the same as you in {0} of {1} tasks ({2}). +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T118785941::INFORMAL"] = "The judge chose the same as you in {0} of {1} tasks ({2})." + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1422468824"] = "Your messages on average" + +-- Your messages on average +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1422468824::INFORMAL"] = "Your messages on average" + +-- After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1709474261"] = "After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models." + +-- After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1709474261::INFORMAL"] = "After you saw the result, you decided {0} more tasks: {1} won {2}, {3} won {4}, and {5} ended equally. They do not count above, since you knew the models." + +-- Result of the series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1779013167"] = "Result of the series" + +-- Your effort with each model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1872744172"] = "Your effort with each model" + +-- Your effort with each model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T1872744172::INFORMAL"] = "Your effort with each model" + +-- Median time +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2152829620"] = "Median time" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2189814010"] = "Model" + +-- The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2579182820"] = "The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there." + +-- The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T2579182820::INFORMAL"] = "The model in the first chat won {0} of these tasks, the model in the second chat {1}. If the second chat wins much more often, knowing the task better may have helped you there." + +-- Decided tasks +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3224295221"] = "Decided tasks" + +-- You decided no task of this series without knowing the models, so it has no result. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3510794091"] = "You decided no task of this series without knowing the models, so it has no result." + +-- You decided no task of this series without knowing the models, so it has no result. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESRESULT::T3510794091::INFORMAL"] = "You decided no task of this series without knowing the models, so it has no result." + +-- classic RAG +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1197693637"] = "classic RAG" + +-- Profile: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1311539038"] = "Profile: {0}" + +-- Data sources: {0} ({1}) +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T1413816655"] = "Data sources: {0} ({1})" + +-- No profile +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2028602035"] = "No profile" + +-- No data sources +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2502582508"] = "No data sources" + +-- No tools +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T2868032641"] = "No tools" + +-- Profile: no longer available +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3128799223"] = "Profile: no longer available" + +-- {0} no longer available +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3209274018"] = "{0} no longer available" + +-- Tools: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T3940425864"] = "Tools: {0}" + +-- Semantic Search +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESSETUPLINE::T853993291"] = "Semantic Search" + +-- After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T1606451454"] = "After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task." + +-- After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T1606451454::INFORMAL"] = "After your own vote on a task, a third model reads both chats and gives its own verdict, without knowing which model wrote which chat. It applies your expectations, values, and goals to every task in the same way and so gives you a second opinion; in the result of the series, you see how often it agreed with you. Since the judge reads both chats in full, it must be allowed to see your data sources as well. It costs one more request per task." + +-- Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T2709626367"] = "Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them." + +-- Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T2709626367::INFORMAL"] = "Both models work with the same profile, the same tools, and the same data sources, so that the comparison stays fair. AI Studio therefore offers only what each of them may use: the model with the lower confidence decides which tools and data sources you can choose, and the tool selection lists the others with the reason. When both models can search the data sources themselves, they do so with Semantic Search; otherwise, AI Studio searches them with every message for both. In each task, you can attach files to your message, which both models receive; images only when both models can read them." + +-- Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T326613437"] = "Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series." + +-- Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T326613437::INFORMAL"] = "Choose the two models you want to compare, e.g., the model you work with every day and a new one. For every task, AI Studio decides at random which model you chat with first, and it hides their names until you look at the result of the series." + +-- A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T49281341"] = "A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind." + +-- A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T49281341::INFORMAL"] = "A task runs in four steps. First, you start it, and AI Studio chooses at random which of the two models you chat with first. Second, you work on your task in the chat as you always do: you can attach files, and the model uses the profile, tools, and data sources of the series. Third, once you are done, the other model gets your first message with its files, and you continue the same task with it. Fourth, you decide which chat helped you more; when the series has a judge, it gives its own verdict afterward. You can look at the result of the series whenever you like. Since it names the models, the tasks you decide afterward no longer count as blind." + +-- A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T863629402"] = "A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series." + +-- A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESTEXTS::T863629402::INFORMAL"] = "A comparison series collects your real tasks over days or weeks. You work on each task with one of the two models, and then on the same task with the other one; the second chat starts with your first message of the first one. Afterward, you decide which chat helped you more. You learn which model was which only when you look at the result of the series." + +-- The judge has not judged this task yet. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T126436094"] = "The judge has not judged this task yet." + +-- The judge preferred chat {0}, while you preferred the other one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1611643242"] = "The judge preferred chat {0}, while you preferred the other one." + +-- The judge preferred chat {0}, while you preferred the other one. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1611643242::INFORMAL"] = "The judge preferred chat {0}, while you preferred the other one." + +-- Judge on task {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1798480084"] = "Judge on task {0}" + +-- The judge found both chats equally helpful, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1811336012"] = "The judge found both chats equally helpful, while you preferred one of them." + +-- The judge found both chats equally helpful, while you preferred one of them. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T1811336012::INFORMAL"] = "The judge found both chats equally helpful, while you preferred one of them." + +-- The judge reads both chats. This can take a while. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T185857423"] = "The judge reads both chats. This can take a while." + +-- The judge preferred chat {0}, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T2805496490"] = "The judge preferred chat {0}, as you did." + +-- The judge preferred chat {0}, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T2805496490::INFORMAL"] = "The judge preferred chat {0}, as you did." + +-- Ask the judge +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3611576738"] = "Ask the judge" + +-- The judge preferred chat {0}, while you found both chats equally helpful. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3944557605"] = "The judge preferred chat {0}, while you found both chats equally helpful." + +-- The judge preferred chat {0}, while you found both chats equally helpful. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T3944557605::INFORMAL"] = "The judge preferred chat {0}, while you found both chats equally helpful." + +-- Both chats together are too long for the judge, so it did not judge this task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4003597607"] = "Both chats together are too long for the judge, so it did not judge this task." + +-- The judge is no longer available as it was when the series began, so it did not judge this task. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4173102270"] = "The judge is no longer available as it was when the series began, so it did not judge this task." + +-- The judge did not give a usable verdict. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T4278818434"] = "The judge did not give a usable verdict." + +-- The judge found both chats equally helpful, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T970331360"] = "The judge found both chats equally helpful, as you did." + +-- The judge found both chats equally helpful, as you did. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVERDICT::T970331360::INFORMAL"] = "The judge found both chats equally helpful, as you did." + +-- Show the result +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1015029476"] = "Show the result" + +-- Test another task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1029988487"] = "Test another task" + +-- Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1119857700"] = "Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more." + +-- Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1119857700::INFORMAL"] = "Now work on the same task with the other model. It got your first message already. Once you are done, you decide which chat helped you more." + +-- One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T116035530"] = "One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew." + +-- One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T116035530::INFORMAL"] = "One of the two models is no longer available as it was when the series began: its provider was deleted, or it uses another model now. You cannot work on another task in this series, but you can start a new series with its setup and choose the model anew." + +-- You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1163964876"] = "You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway?" + +-- You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1163964876::INFORMAL"] = "You have seen the result of this series, so you know which model answers in which chat. Another task therefore no longer counts as blind: the result shows it apart, and the results of all comparisons leave it out. To compare blindly again, start a new series with the setup of this one. Do you want to test another task in this series anyway?" + +-- Task {0}: chat 1 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T134264261"] = "Task {0}: chat 1" + +-- Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T174840209"] = "Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it." + +-- Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T174840209::INFORMAL"] = "Work on your task with this model, as you would in a chat. Once you are done, the other model gets the same first message, and you continue with it." + +-- AI Studio could not save the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1811212186"] = "AI Studio could not save the comparison series. Please try again." + +-- AI Studio could not save the comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T1811212186::INFORMAL"] = "AI Studio could not save the comparison series. Please try again." + +-- How a task works +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2064032605"] = "How a task works" + +-- Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2374282918"] = "Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first." + +-- Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2374282918::INFORMAL"] = "Start your first task. You work on it with one of the two models first, and then with the other one. AI Studio chooses at random which model you meet first." + +-- I'm done with this model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2855215253"] = "I'm done with this model" + +-- Discard task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T2966310514"] = "Discard task" + +-- Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3114690087"] = "Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone." + +-- Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3114690087::INFORMAL"] = "Are you sure you want to discard this task with both its chats? It does not count toward the result. This cannot be undone." + +-- You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3203252119"] = "You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual." + +-- You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3203252119::INFORMAL"] = "You cannot attach images in this series, since not both models can read them. Other files, such as documents, work as usual." + +-- The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3217444240"] = "The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result?" + +-- The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3217444240::INFORMAL"] = "The result tells you which model answered in which chat. Tasks you decide afterward no longer count as blind: the result shows them apart, and the results of all comparisons leave them out. Do you want to see the result?" + +-- Decided tasks so far: {0}. Do you want to test another task? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3225301908"] = "Decided tasks so far: {0}. Do you want to test another task?" + +-- Decided tasks so far: {0}. Do you want to test another task? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3225301908::INFORMAL"] = "Decided tasks so far: {0}. Do you want to test another task?" + +-- Task {0}: your decision +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3586181851"] = "Task {0}: your decision" + +-- Task {0}: your decision +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3586181851::INFORMAL"] = "Task {0}: your decision" + +-- Start a task +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3692047609"] = "Start a task" + +-- Setup of this series +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T3798429422"] = "Setup of this series" + +-- Hide the result +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T4133577695"] = "Hide the result" + +-- Task {0}: chat 2 +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSERIESVIEW::T83931404"] = "Task {0}: chat 2" + +-- Complete answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T1705377513"] = "Complete answers" + +-- Model +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T2189814010"] = "Model" + +-- Until the first token +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T2818188291"] = "Until the first token" + +-- The median time each model took, so that a single slow answer does not distort the picture. Only complete answers count. The times also depend on the length of the answers and on how busy the servers were. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T357780752"] = "The median time each model took, so that a single slow answer does not distort the picture. Only complete answers count. The times also depend on the length of the answers and on how busy the servers were." + +-- Until the complete answer +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONSPEEDS::T517769867"] = "Until the complete answer" + +-- A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T1958437853"] = "A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run." + +-- A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T1958437853::INFORMAL"] = "A judge is a third model which reads the request and both answers of each run and decides which answer is better, just as blindly as you do. It applies your expectations, values, and goals to every run in the same way and so gives you a second opinion. At the end, you see how often the judge agreed with you: when it rarely does, your votes or your description deserve a second look. You see its verdicts only after your own votes, and it costs one more request per run." + +-- Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2158946259"] = "Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge." + +-- Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2158946259::INFORMAL"] = "Every model answers a little differently each time, so a single comparison says little. With several runs, AI Studio asks both models the same request again and again, and you vote on every run blindly. The more runs, the more the result tells you, but each run costs one request per model, and one more with a judge." + +-- The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2347517054"] = "The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own." + +-- The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T2347517054::INFORMAL"] = "The material is a text which both models work on, e.g., a document to summarize, an email to answer, or code to review. Paste it, or load it from a file, e.g., from a PDF document, whose text AI Studio then reads. Leave it empty when your question stands on its own." + +-- Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T3491184146"] = "Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”." + +-- Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T3491184146::INFORMAL"] = "Both models receive your question or task and the material below, and nothing else: no tools, no data sources, no mailboxes, and no web pages, even when you use them in the chat. A web address in your request stays plain text, which no model opens. That way, the answers show what the models themselves make of the same text. To compare how well two models help you with tools, data sources, and your own files, use the mode “Everyday Tasks”." + +-- Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T477339319"] = "Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again." + +-- Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONTEXTS::T477339319::INFORMAL"] = "Choose the two models you want to compare. Both receive the same request, and AI Studio shows their answers side by side, in a random order and without their names: you vote for the better answer blindly and learn only at the end which model wrote which. You may also choose the same model twice. Then the comparison shows how much its answers vary when you ask it the same thing again." + +-- Answer A +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T1420338340"] = "Answer A" + +-- Answer B +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T1470671197"] = "Answer B" + +-- Which answer is better? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2388547151"] = "Which answer is better?" + +-- Finish +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2670325426"] = "Finish" + +-- Both are equally good +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T2723559248"] = "Both are equally good" + +-- Answer A is better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3123852190"] = "Answer A is better" + +-- Skip remaining votes +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3188579167"] = "Skip remaining votes" + +-- Skip this run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3334593867"] = "Skip this run" + +-- Next run +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3533713133"] = "Next run" + +-- At least one of the models did not answer in this run, so there is nothing to compare. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3561467042"] = "At least one of the models did not answer in this run, so there is nothing to compare." + +-- Run {0} of {1} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T3815324322"] = "Run {0} of {1}" + +-- Answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T46578788"] = "Answers" + +-- You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T651204000"] = "You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end." + +-- You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T651204000::INFORMAL"] = "You do not know which model wrote which answer, and the columns are in random order. Compare them by what they say. You learn the names at the end." + +-- Answer B is better +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MODELCOMPARISON::MODELCOMPARISONVOTING::T933724747"] = "Answer B is better" + -- You can enter text, attach one or more documents, or use both. At least one input is required. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required." @@ -6711,6 +7695,24 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T713722424"] = "AI Studio could no -- AI Studio could not store what the AI received separately from your chat. It remains in the chat itself, so you can still read it. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTTEXT::T713722424::INFORMAL"] = "AI Studio could not store what the AI received separately from your chat. It remains in the chat itself, so you can still read it." +-- The selected provider may not see this conversation. Please select another provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1132710328"] = "The selected provider may not see this conversation. Please select another provider." + +-- The selected provider may not see this conversation. Please select another provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1132710328::INFORMAL"] = "The selected provider may not see this conversation. Please select another provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1809312323"] = "Please select a provider." + +-- Please select a provider. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T1809312323::INFORMAL"] = "Please select a provider." + +-- AI Studio could not save this conversation. Please try again. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T463511884"] = "AI Studio could not save this conversation. Please try again." + +-- AI Studio could not save this conversation. Please try again. +UI_TEXT_CONTENT["AISTUDIO::CHAT::EMBEDDED::EMBEDDEDCHATSERVICE::T463511884::INFORMAL"] = "AI Studio could not save this conversation. Please try again." + -- The local image file does not exist. Skipping the image. UI_TEXT_CONTENT["AISTUDIO::CHAT::IMAGESOURCEEXTENSIONS::T255679918"] = "The local image file does not exist. Skipping the image." @@ -7548,6 +8550,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T346886000"] = "Plugins of y -- Plugins of your organization. Your organization brings them back. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T346886000::INFORMAL"] = "Plugins of your organization. Your organization brings them back." +-- Your presets, results, and comparison series with their chats in the Model Comparison assistant. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3574579770"] = "Your presets, results, and comparison series with their chats in the Model Comparison assistant." + +-- Your presets, results, and comparison series with their chats in the Model Comparison assistant. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3574579770::INFORMAL"] = "Your presets, results, and comparison series with their chats in the Model Comparison assistant." + -- AI Studio keeps your data in two folders on this computer. When you back up your computer with a tool of your own, such as [Restic](https://restic.net), [Borg](https://www.borgbackup.org), or [Time Machine](https://support.apple.com/en-us/104984), this shows which of their contents belong in the backup and which it can leave out. Click a path to copy it to the clipboard, or use the folder button to open it in your file manager. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATAFOLDERS::T3596708716"] = "AI Studio keeps your data in two folders on this computer. When you back up your computer with a tool of your own, such as [Restic](https://restic.net), [Borg](https://www.borgbackup.org), or [Time Machine](https://support.apple.com/en-us/104984), this shows which of their contents belong in the backup and which it can leave out. Click a path to copy it to the clipboard, or use the folder button to open it in your file manager." @@ -7932,6 +8940,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T147584079"] = "You -- You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T147584079::INFORMAL"] = "You switched Semantic Search off in the tool settings, so AI Studio uses classic RAG instead and searches your data sources with every message." +-- One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1491430062"] = "One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + +-- One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1491430062::INFORMAL"] = "One of the selected models cannot use tools, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + -- Data Source Selection UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T15302104"] = "Data Source Selection" @@ -7977,6 +8991,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2382076848"] = "Whe -- When the model of a chat cannot use tools, AI Studio uses classic RAG instead and searches your data sources with every message. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2382076848::INFORMAL"] = "When the model of a chat cannot use tools, AI Studio uses classic RAG instead and searches your data sources with every message." +-- One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2654482616"] = "One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + +-- One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2654482616::INFORMAL"] = "One of the selected providers is not trusted enough for Semantic Search, so AI Studio uses classic RAG for all of them instead and searches your data sources with every message." + -- Whenever a question calls for it, the AI picks the fitting ones among these data sources itself. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2854302511"] = "Whenever a question calls for it, the AI picks the fitting ones among these data sources itself." @@ -8139,6 +9159,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T244391 -- Change what this tile opens UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERSETTINGSACTION::T4272203100"] = "Change what this tile opens" +-- Stop +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T1266644741"] = "Stop" + +-- AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T1561705002"] = "AI Studio cannot open this conversation. Its file may be damaged, or a newer version of AI Studio stored it." + +-- Export message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T274919936"] = "Export message" + +-- Your message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T3642105949"] = "Your message" + +-- Your message +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T3642105949::INFORMAL"] = "Your message" + +-- Send +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T4069397711"] = "Send" + +-- approx. {0} tokens +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::EMBEDDEDCHAT::T857715435"] = "approx. {0} tokens" + -- Copy the path: {0} UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::FILEACTIONS::T1140628382"] = "Copy the path: {0}" @@ -9834,6 +10875,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T749664565"] = "Tool Selec -- No tool can be selected right now. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T953828816"] = "No tool can be selected right now." +-- This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T966234001"] = "This tool requires provider confidence {0}. The selected provider with the lowest confidence has {1}." + -- Select tools UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T998515990"] = "Select tools" @@ -12570,6 +13614,240 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Clos -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::INFORMATIONDIALOG::T3448155331"] = "Close" +-- Compared +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1058933136"] = "Compared" + +-- Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1167627412"] = "Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone." + +-- Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1167627412::INFORMAL"] = "Are you sure you want to delete all results of the mode “Answer Variance” and all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets stay. This cannot be undone." + +-- {0}: {1} comparisons with {2} runs. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T119967148"] = "{0}: {1} comparisons with {2} runs." + +-- All results +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1647689906"] = "All results" + +-- Speed +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T174817312"] = "Speed" + +-- Same choice +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1835176194"] = "Same choice" + +-- Delete all comparison series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T1848485659"] = "Delete all comparison series" + +-- AI Studio could not delete all comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2092205828"] = "AI Studio could not delete all comparison series. Please try again." + +-- AI Studio could not delete all comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2092205828::INFORMAL"] = "AI Studio could not delete all comparison series. Please try again." + +-- Judge +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2094884932"] = "Judge" + +-- AI Studio could not delete all results. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2104729496"] = "AI Studio could not delete all results. Please try again." + +-- AI Studio could not delete all results. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2104729496::INFORMAL"] = "AI Studio could not delete all results. Please try again." + +-- No two different models were compared with each other yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2439811736"] = "No two different models were compared with each other yet." + +-- You have not voted on two different models yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2482334433"] = "You have not voted on two different models yet." + +-- You have not voted on two different models yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2482334433::INFORMAL"] = "You have not voted on two different models yet." + +-- No judge has given a usable verdict yet. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T265789464"] = "No judge has given a usable verdict yet." + +-- How often each model won by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T268255753"] = "How often each model won by the verdicts of the judges." + +-- Agreement +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2750704103"] = "Agreement" + +-- How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2888156258"] = "How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers." + +-- How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T2888156258::INFORMAL"] = "How often each model won by your votes. A comparison of a model with itself does not count here, since it only shows how consistently that model answers." + +-- Ranking by the judges +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3071310071"] = "Ranking by the judges" + +-- Agreement with your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3243788701"] = "Agreement with your votes" + +-- Agreement with your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3243788701::INFORMAL"] = "Agreement with your votes" + +-- Judges +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3347779221"] = "Judges" + +-- {0}: {1} comparison series with {2} tasks decided without knowing the models. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3355735368"] = "{0}: {1} comparison series with {2} tasks decided without knowing the models." + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3448155331"] = "Close" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T360017507"] = "Your votes" + +-- Your votes +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T360017507::INFORMAL"] = "Your votes" + +-- There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3704633531"] = "There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”." + +-- There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3704633531::INFORMAL"] = "There are no results yet. They appear here as soon as you vote on the answers in the mode “Answer Variance”, or once you look at the result of a comparison series in the mode “Everyday Tasks”." + +-- How often each of two models won when they competed directly, by your votes and by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T372027252"] = "How often each of two models won when they competed directly, by your votes and by the verdicts of the judges." + +-- How often each of two models won when they competed directly, by your votes and by the verdicts of the judges. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T372027252::INFORMAL"] = "How often each of two models won when they competed directly, by your votes and by the verdicts of the judges." + +-- How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3831905913"] = "How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count." + +-- How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3831905913::INFORMAL"] = "How often each judge chose the same as you. Only runs and tasks with both your vote and a verdict of the judge count." + +-- Delete all results and comparison series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T3967608788"] = "Delete all results and comparison series" + +-- Delete the results of “Answer Variance” +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T4059958648"] = "Delete the results of “Answer Variance”" + +-- Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T726138079"] = "Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone." + +-- Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T726138079::INFORMAL"] = "Are you sure you want to delete all results of the mode “Answer Variance”? Your presets and the comparison series of the mode “Everyday Tasks” stay. This cannot be undone." + +-- Pairs +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T865427974"] = "Pairs" + +-- Only the mode “Answer Variance” measures how fast the models answer. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T922668471"] = "Only the mode “Answer Variance” measures how fast the models answer." + +-- Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T93821586"] = "Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone." + +-- Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONRESULTSDIALOG::T93821586::INFORMAL"] = "Are you sure you want to delete all comparison series of the mode “Everyday Tasks” with their chats, also those whose result you have not seen yet? Your presets and the results of the mode “Answer Variance” stay. This cannot be undone." + +-- Judge (optional) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1206060603"] = "Judge (optional)" + +-- This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T134968648"] = "This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series." + +-- This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T134968648::INFORMAL"] = "This series starts with the setup of the series '{0}'. You can change all of it before you create the series. The tasks stay in their series." + +-- Please choose two different models. A series compares how well two models help you. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1531478547"] = "Please choose two different models. A series compares how well two models help you." + +-- Please choose two different models. A series compares how well two models help you. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T1531478547::INFORMAL"] = "Please choose two different models. A series compares how well two models help you." + +-- Create series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2037222750"] = "Create series" + +-- Name of the series +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2083243781"] = "Name of the series" + +-- No judge +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2562272449"] = "No judge" + +-- Name +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T266367750"] = "Name" + +-- The profile no longer exists, so the series starts without a profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2782528640"] = "The profile no longer exists, so the series starts without a profile." + +-- The profile no longer exists, so the series starts without a profile. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T2782528640::INFORMAL"] = "The profile no longer exists, so the series starts without a profile." + +-- Models +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T30216107"] = "Models" + +-- The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3472943722"] = "The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3472943722::INFORMAL"] = "The second model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- Ask a judge? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3641185871"] = "Ask a judge?" + +-- Tools: {0}, data sources: {1} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3667830435"] = "Tools: {0}, data sources: {1}" + +-- The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3712631176"] = "The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3712631176::INFORMAL"] = "The first model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose it anew." + +-- AI Studio could not save the new comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3741421828"] = "AI Studio could not save the new comparison series. Please try again." + +-- AI Studio could not save the new comparison series. Please try again. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3741421828::INFORMAL"] = "AI Studio could not save the new comparison series. Please try again." + +-- Profile, tools, and data sources +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T3936393556"] = "Profile, tools, and data sources" + +-- Choose both models first: what you can select here depends on them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4030857378"] = "Choose both models first: what you can select here depends on them." + +-- Choose both models first: what you can select here depends on them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4030857378::INFORMAL"] = "Choose both models first: what you can select here depends on them." + +-- First model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4044223184"] = "First model" + +-- A judge assesses every task +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T4111877217"] = "A judge assesses every task" + +-- Some data sources are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T511469979"] = "Some data sources are no longer available, so the series starts without them." + +-- Some data sources are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T511469979::INFORMAL"] = "Some data sources are no longer available, so the series starts without them." + +-- Some tools are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T600304160"] = "Some tools are no longer available, so the series starts without them." + +-- Some tools are no longer available, so the series starts without them. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T600304160::INFORMAL"] = "Some tools are no longer available, so the series starts without them." + +-- e.g., Weekly reports +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T795365587"] = "e.g., Weekly reports" + +-- The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T805855292"] = "The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off." + +-- The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T805855292::INFORMAL"] = "The judge model is no longer available, uses another model now, or may no longer be used by this assistant. Please choose another one, or switch the judge off." + +-- Second model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T867467332"] = "Second model" + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T900713019"] = "Cancel" + +-- Judge model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::MODELCOMPARISONSERIESDIALOG::T953975257"] = "Judge model" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." @@ -14577,6 +15855,66 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4049873 -- Preselect the web content reader? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Preselect the web content reader?" +-- No preset preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1028082569"] = "No preset preselected" + +-- Preselected preset +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1094542630"] = "Preselected preset" + +-- The mode “Answer Variance” opens with this preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1310335465"] = "The mode “Answer Variance” opens with this preset." + +-- No Model Comparison options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T1495085044"] = "No Model Comparison options are preselected" + +-- Model Comparison options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2330752759"] = "Model Comparison options are preselected" + +-- Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2350726718"] = "Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model." + +-- Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2350726718::INFORMAL"] = "Every new preset of the mode “Answer Variance” and every new comparison series of the mode “Everyday Tasks” starts with this model as its first model." + +-- The preselected preset no longer exists. Please select another preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2696863075"] = "The preselected preset no longer exists. Please select another preset." + +-- The preselected preset no longer exists. Please select another preset. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2696863075::INFORMAL"] = "The preselected preset no longer exists. Please select another preset." + +-- No model preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2802610587"] = "No model preselected" + +-- When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2982794040"] = "When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against." + +-- When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T2982794040::INFORMAL"] = "When enabled, new presets and comparison series start with the models you preselect here, e.g., with the model you work with every day and want to test new models against." + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3448155331"] = "Close" + +-- Preselect Model Comparison options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3640087463"] = "Preselect Model Comparison options?" + +-- Preselected judge model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3746033265"] = "Preselected judge model" + +-- A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3884208586"] = "A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on." + +-- A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T3884208586::INFORMAL"] = "A preselected judge model switches the judge on in every new preset and every new comparison series. Without one, the judge stays off until you switch it on." + +-- Missing preset ({0}) +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T413828499"] = "Missing preset ({0})" + +-- Assistant: Model Comparison Options +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T4212945869"] = "Assistant: Model Comparison Options" + +-- Preselected model +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMODELCOMPARISON::T473925378"] = "Preselected model" + -- Choose whether the assistant should use the app default profile, no profile, or a specific profile. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." @@ -15969,6 +17307,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1777666968"] = "Prompt Optimizer" -- SWOT Analysis UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1864542518"] = "SWOT Analysis" +-- Compare two models in a blind test for how much their answers vary and how useful they are. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1869142375"] = "Compare two models in a blind test for how much their answers vary and how useful they are." + +-- Compare two models in a blind test for how much their answers vary and how useful they are. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1869142375::INFORMAL"] = "Compare two models in a blind test for how much their answers vary and how useful they are." + -- Slide Assistant UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Slide Assistant" @@ -16083,6 +17427,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3881504200"] = "Use an LLM to fin -- Use an LLM to find an icon for a given context. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3881504200::INFORMAL"] = "Use an LLM to find an icon for a given context." +-- Model Comparison +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3910956815"] = "Model Comparison" + -- Job Posting UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3930052338"] = "Job Posting" @@ -17787,6 +19134,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It -- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again. UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511::INFORMAL"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again." +-- hidden +UI_TEXT_CONTENT["AISTUDIO::PROVIDER::PROVIDERIDENTITYEXTENSIONS::T4128829753"] = "hidden" + -- Text too long UI_TEXT_CONTENT["AISTUDIO::PROVIDER::PROVIDERREQUESTFAILUREREASONEXTENSIONS::T1074711534"] = "Text too long" @@ -18480,6 +19830,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T555062689"] = "Log View -- New Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T826248509"] = "New Chat" +-- Model Comparison Assistant +UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T941908687"] = "Model Comparison Assistant" + -- Trust LLM providers from the USA UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T1748300640"] = "Trust LLM providers from the USA" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 1bb34b16c..0ae15062a 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -1,7 +1,9 @@ using AIStudio.Agents; using AIStudio.Agents.AssistantAudit; using AIStudio.Assistants.BeyondFactsLab; +using AIStudio.Assistants.ModelComparison; using AIStudio.Assistants.VisualBriefing; +using AIStudio.Chat.Embedded; using AIStudio.Settings; using AIStudio.Tools.ToolCallingSystem; using AIStudio.Tools.Databases; @@ -236,6 +238,9 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index ae2a0f9ba..c1a5f8a1c 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -91,6 +91,9 @@ public abstract class BaseProvider : IProvider, ISecretId /// public abstract string InstanceName { get; set; } + /// + public bool HidesIdentity { get; set; } + /// public string AdditionalJsonApiParameters { get; init; } = string.Empty; @@ -265,10 +268,10 @@ public abstract class BaseProvider : IProvider, ISecretId protected virtual string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason, ContextWindow contextWindow = default) => failureReason switch { ProviderRequestFailureReason.TOO_MANY_REQUESTS => TA("The provider rejected the request because too many requests were sent. Please wait a moment and try again."), - ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TA("The API key for the provider '{0}' is missing or was rejected. Please check the key in the settings."), this.InstanceName), - ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TA("The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."), this.InstanceName), - ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => string.Format(TA("The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."), this.InstanceName), - ProviderRequestFailureReason.MODEL_NOT_FOUND => string.Format(TA("The provider '{0}' does not know the selected model. Please select another model."), this.InstanceName), + ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TA("The API key for the provider '{0}' is missing or was rejected. Please check the key in the settings."), this.NameForUser), + ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TA("The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."), this.NameForUser), + ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => string.Format(TA("The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."), this.NameForUser), + ProviderRequestFailureReason.MODEL_NOT_FOUND => string.Format(TA("The provider '{0}' does not know the selected model. Please select another model."), this.NameForUser), // // Naming the number is the whole point of knowing it: "too long" leaves the user guessing // by how much, while the window turns the next step into arithmetic. Where nobody knows the @@ -276,13 +279,13 @@ public abstract class BaseProvider : IProvider, ISecretId // // Written out in full rather than shortened the way the chat shortens it. The sentence ends // by asking the user to set a chunk size, and 32.77k is not a number anybody types into a - // field. + // field. A provider which hides its identity names no number either, since the window tells models apart. // - ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED when contextWindow.IsKnown => string.Format(TA("The text was longer than the selected model accepts, which is {0} tokens. Please select a model which takes longer texts, or reduce the chunk size of the data source."), contextWindow.DefaultTokens.ToString("N0", I18N.I.Culture)), + ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED when contextWindow.IsKnown && !this.HidesIdentity =>string.Format(TA("The text was longer than the selected model accepts, which is {0} tokens. Please select a model which takes longer texts, or reduce the chunk size of the data source."), contextWindow.DefaultTokens.ToString("N0", I18N.I.Culture)), ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TA("The text was longer than the selected model accepts. Please select a model which takes longer texts, or reduce the chunk size of the data source."), - ProviderRequestFailureReason.TOOLS_NOT_SUPPORTED => string.Format(TA("The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there."), this.InstanceName), - ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => string.Format(TA("The provider '{0}' cannot create embeddings. Please select a provider which offers an embedding model."), this.InstanceName), - ProviderRequestFailureReason.INVALID_RESPONSE => string.Format(TB("The provider '{0}' sent an answer AI Studio was not able to read."), this.InstanceName), + ProviderRequestFailureReason.TOOLS_NOT_SUPPORTED => string.Format(TA("The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there."), this.NameForUser), + ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => string.Format(TA("The provider '{0}' cannot create embeddings. Please select a provider which offers an embedding model."), this.NameForUser), + ProviderRequestFailureReason.INVALID_RESPONSE => string.Format(TB("The provider '{0}' sent an answer AI Studio was not able to read."), this.NameForUser), _ => string.Empty, }; @@ -322,8 +325,8 @@ public abstract class BaseProvider : IProvider, ISecretId { var providerMessage = ReadProviderErrorMessage(responseBody); userMessage = string.IsNullOrWhiteSpace(providerMessage) - ? string.Format(TB("The provider '{0}' rejected the embedding request with the status code {1}."), this.InstanceName, (int)statusCode) - : string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, providerMessage); + ? string.Format(TB("The provider '{0}' rejected the embedding request with the status code {1}."), this.NameForUser, (int)statusCode) + : string.Format(TB("The provider '{0}' reported an error: {1}"), this.NameForUser, providerMessage); } return new(failureReason, userMessage, statusCode, reasonPhrase, responseBody); @@ -339,7 +342,7 @@ public abstract class BaseProvider : IProvider, ISecretId if (isTimeout) return new(ProviderRequestFailureReason.PROVIDER_UNAVAILABLE, this.GetProviderRequestFailureUserMessage(ProviderRequestFailureReason.PROVIDER_UNAVAILABLE), responseBody: exception.Message); - return new(ProviderRequestFailureReason.UNKNOWN, string.Format(TB("The embedding request to the provider '{0}' failed: {1}"), this.InstanceName, exception.Message), responseBody: exception.Message); + return new(ProviderRequestFailureReason.UNKNOWN, string.Format(TB("The embedding request to the provider '{0}' failed: {1}"), this.NameForUser, exception.Message), responseBody: exception.Message); } /// @@ -513,8 +516,8 @@ public abstract class BaseProvider : IProvider, ISecretId if (string.IsNullOrWhiteSpace(userMessage)) { userMessage = string.IsNullOrWhiteSpace(errorMessage) - ? string.Format(TB("The provider '{0}' reported an error while streaming the response."), this.InstanceName) - : string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, errorMessage); + ? string.Format(TB("The provider '{0}' reported an error while streaming the response."), this.NameForUser) + : string.Format(TB("The provider '{0}' reported an error: {1}"), this.NameForUser, errorMessage); } this.logger.LogError("The {ProviderName} stream returned an error for provider '{ProviderInstanceName}' (provider={ProviderType}). EventType={StreamEventType}, ErrorCode={ErrorCode}, ErrorType={ErrorType}, ErrorMessage='{ErrorMessage}', Body='{ErrorBody}'", providerName, this.InstanceName, this.Provider, eventType, errorCode, errorType, errorMessage, jsonData); @@ -702,7 +705,7 @@ public abstract class BaseProvider : IProvider, ISecretId /// /// What the provider itself said about the failure. /// The message to show. - private string GetContextTooLargeUserMessage(string? providerMessage) => string.Format(TA("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments and the passages of your data sources, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.InstanceName, this.Provider, providerMessage); + private string GetContextTooLargeUserMessage(string? providerMessage) => string.Format(TA("We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments and the passages of your data sources, is probably too large for the selected model and provider. The provider message is: '{2}'"), this.NameForUser, this.TypeForUser,providerMessage); /// /// Sends a request and handles rate limiting by exponential backoff. @@ -776,7 +779,7 @@ public abstract class BaseProvider : IProvider, ISecretId if (nextResponse.StatusCode is HttpStatusCode.Forbidden) { - await MessageBus.INSTANCE.SendError(new(AppIcons.BLOCKED, string.Format(TA("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(AppIcons.BLOCKED, string.Format(TA("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.NameForUser, this.TypeForUser,nextResponse.ReasonPhrase))); this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; failureAlreadyExplained = true; @@ -838,7 +841,7 @@ public abstract class BaseProvider : IProvider, ISecretId } else { - await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The required message format might be changed. The provider message is: '{2}'"), this.InstanceName, this.Provider, badRequestMessage))); + await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The required message format might be changed. The provider message is: '{2}'"), this.NameForUser, this.TypeForUser,badRequestMessage))); } this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); @@ -849,7 +852,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.NotFound) { - await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Something was not found. The provider message is: '{2}'"), this.NameForUser, this.TypeForUser,nextResponse.ReasonPhrase))); this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; failureAlreadyExplained = true; @@ -858,7 +861,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.Unauthorized) { - await MessageBus.INSTANCE.SendError(new(AppIcons.KEY, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The API key might be invalid. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(AppIcons.KEY, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The API key might be invalid. The provider message is: '{2}'"), this.NameForUser, this.TypeForUser,nextResponse.ReasonPhrase))); this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; failureAlreadyExplained = true; @@ -867,7 +870,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.InternalServerError) { - await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"), this.NameForUser, this.TypeForUser,nextResponse.ReasonPhrase))); this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; failureAlreadyExplained = true; @@ -876,7 +879,7 @@ public abstract class BaseProvider : IProvider, ISecretId if(nextResponse.StatusCode is HttpStatusCode.ServiceUnavailable) { - await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The provider is overloaded. The message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase))); + await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The provider is overloaded. The message is: '{2}'"), this.NameForUser, this.TypeForUser,nextResponse.ReasonPhrase))); this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; failureAlreadyExplained = true; @@ -901,7 +904,7 @@ public abstract class BaseProvider : IProvider, ISecretId if (string.IsNullOrWhiteSpace(refusalMessage)) refusalMessage = nextResponse.ReasonPhrase; - await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'"), this.InstanceName, this.Provider, (int)nextResponse.StatusCode, refusalMessage))); + await MessageBus.INSTANCE.SendError(new(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). The provider turned the request down with the status code {2} and would turn it down again, so we stopped trying. The provider message is: '{3}'"), this.NameForUser, this.TypeForUser,(int)nextResponse.StatusCode, refusalMessage))); this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody); errorMessage = nextResponse.ReasonPhrase; failureAlreadyExplained = true; @@ -942,7 +945,7 @@ public abstract class BaseProvider : IProvider, ISecretId // which explains nothing is the one arriving last. // if(!failureAlreadyExplained) - await MessageBus.INSTANCE.SendError(new DataErrorMessage(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Even after {2} retries, there were some problems with the request. The provider message is: '{3}'."), this.InstanceName, this.Provider, MAX_RETRIES, errorMessage))); + await MessageBus.INSTANCE.SendError(new DataErrorMessage(AppIcons.PROVIDER_ERROR, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Even after {2} retries, there were some problems with the request. The provider message is: '{3}'."), this.NameForUser, this.TypeForUser,MAX_RETRIES, errorMessage))); return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response); } @@ -1001,7 +1004,7 @@ public abstract class BaseProvider : IProvider, ISecretId } else { - await MessageBus.INSTANCE.SendError(new(AppIcons.ANSWER_STREAM, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message))); + await MessageBus.INSTANCE.SendError(new(AppIcons.ANSWER_STREAM, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.NameForUser, e.Message))); this.logger.LogError(e, "Failed to stream the {OperationName} from {ProviderName} '{ProviderInstanceName}': {ErrorMessage}", operationName, providerName, this.InstanceName, e.Message); } } @@ -1046,7 +1049,7 @@ public abstract class BaseProvider : IProvider, ISecretId } else { - await MessageBus.INSTANCE.SendError(new(AppIcons.ANSWER_STREAM, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message))); + await MessageBus.INSTANCE.SendError(new(AppIcons.ANSWER_STREAM, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.NameForUser, e.Message))); this.logger.LogError(e, "Failed to read the stream from {ProviderName} '{ProviderInstanceName}': {ErrorMessage}", providerName, this.InstanceName, e.Message); } @@ -1510,11 +1513,11 @@ public abstract class BaseProvider : IProvider, ISecretId return classifiedMessage; if (IsUndecodableFormatFailure(statusCode, responseBody)) - return string.Format(TA("The provider '{0}' was not able to read the audio file. It probably does not support the WebM/Opus format which AI Studio sends. Please contact the provider about it."), this.InstanceName); + return string.Format(TA("The provider '{0}' was not able to read the audio file. It probably does not support the WebM/Opus format which AI Studio sends. Please contact the provider about it."), this.NameForUser); var providerMessage = ReadProviderErrorMessage(responseBody); if (!string.IsNullOrWhiteSpace(providerMessage)) - return string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, providerMessage); + return string.Format(TB("The provider '{0}' reported an error: {1}"), this.NameForUser, providerMessage); return string.Empty; } diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index e187b166d..d9da77abe 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -28,7 +28,18 @@ public interface IProvider /// e.g., to distinguish between different OpenAI API keys. /// public string InstanceName { get; } - + + /// + /// Whether messages to the user leave out which provider and which model this is, e.g., while the Model Comparison + /// assistant must not reveal which model answers in a chat. + /// + /// + /// Set on the instance of one request, which Settings.Provider.CreateProvider creates anew every time, so that it + /// never reaches another request. Messages read the names through NameForUser, TypeForUser, and ModelNameForUser. + /// What a provider writes into its own error message can still name it; such a message reaches the user as it is. + /// + public bool HidesIdentity { get; set; } + /// /// The additional API parameters. /// diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index 9280577b8..e953bfd6d 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -17,6 +17,9 @@ public class NoProvider : IProvider public string InstanceName { get; set; } = "None"; + /// + public bool HidesIdentity { get; set; } + /// public string AdditionalJsonApiParameters { get; init; } = string.Empty; diff --git a/app/MindWork AI Studio/Provider/ProviderIdentityExtensions.cs b/app/MindWork AI Studio/Provider/ProviderIdentityExtensions.cs new file mode 100644 index 000000000..57adbda9a --- /dev/null +++ b/app/MindWork AI Studio/Provider/ProviderIdentityExtensions.cs @@ -0,0 +1,34 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Provider; + +/// +/// How messages to the user name a provider and its model, see IProvider.HidesIdentity. +/// +public static class ProviderIdentityExtensions +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderIdentityExtensions).Namespace, nameof(ProviderIdentityExtensions)); + + /// The provider of a request. + extension(IProvider provider) + { + /// + /// The name of the provider in a message to the user: its instance name, or a neutral word while it hides + /// which provider it is. + /// + public string NameForUser => provider.HidesIdentity ? TB("hidden") : provider.InstanceName; + + /// + /// The type of the provider in a message to the user, or a neutral word while it hides which provider it is. + /// + public string TypeForUser => provider.HidesIdentity ? TB("hidden") : provider.Provider.ToString(); + + /// + /// Gets the name of a model in a message to the user, or a neutral word while the provider hides which + /// provider it is. + /// + /// The model of the request. + /// The name of the model to show. + public string ModelNameForUser(Model model) => provider.HidesIdentity ? TB("hidden") : model.Id; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs index 343870a0b..586588bea 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -35,6 +35,9 @@ public sealed partial class Routes public const string ASSISTANT_AI_STUDIO_I18N = "/assistant/ai-studio/i18n"; public const string ASSISTANT_DOCUMENT_ANALYSIS = "/assistant/document-analysis"; public const string ASSISTANT_BATCH_PROCESSING = "/assistant/batch-processing"; + public const string ASSISTANT_MODEL_COMPARISON = "/assistant/model-comparison"; + public const string ASSISTANT_MODEL_COMPARISON_STABILITY = "/assistant/model-comparison/stability"; + public const string ASSISTANT_MODEL_COMPARISON_TASKS = "/assistant/model-comparison/tasks"; public const string ASSISTANT_DYNAMIC = "/assistant/dynamic"; public const string ASSISTANT_META_ASSISTANT = "/assistant/builder"; public const string ASSISTANT_LOG_VIEWER = "/assistant/log-viewer"; diff --git a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs index c70f320c2..43fd7885c 100644 --- a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs +++ b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs @@ -30,6 +30,7 @@ public enum ConfigurableAssistant VISUAL_BRIEFING_ASSISTANT, BATCH_PROCESSING_ASSISTANT, BEYOND_FACTS_LAB_ASSISTANT, + MODEL_COMPARISON_ASSISTANT, // ReSharper disable InconsistentNaming I18N_ASSISTANT, diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 36b0c969d..679bcbc14 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -222,7 +222,12 @@ public sealed class Data /// Gets the managed settings of the BeyondFacts Lab. /// public DataBeyondFactsLab BeyondFactsLab { get; set; } = new(x => x.BeyondFactsLab); - + + /// + /// Gets the options of the Model Comparison assistant. + /// + public DataModelComparison ModelComparison { get; set; } = new(x => x.ModelComparison); + public DataLegalCheck LegalCheck { get; set; } = new(); public DataSynonyms Synonyms { get; set; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataModelComparison.cs b/app/MindWork AI Studio/Settings/DataModel/DataModelComparison.cs new file mode 100644 index 000000000..243ad45c9 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataModelComparison.cs @@ -0,0 +1,68 @@ +using System.Linq.Expressions; + +using AIStudio.Provider; + +namespace AIStudio.Settings.DataModel; + +/// +/// Stores the options of the Model Comparison assistant, which an organization may manage except for the preset. +/// +/// The managed-configuration selector. +public sealed class DataModelComparison(Expression>? configSelection = null) +{ + /// + /// The value of a model setting which preselects no model at all. + /// + /// + /// An empty text already means the provider which the whole app preselects, as in every other assistant, so + /// "no model" needs a value of its own. It is the ID of Provider.NONE, an empty GUID, which a configuration + /// plugin can set as well, since the plugin reads these settings as GUIDs. ConfigurationProviderSelection + /// offers the same value for its entry without a provider. + /// + public static readonly string NO_MODEL = Provider.NONE.Id; + + /// + /// Initializes unmanaged options of the Model Comparison assistant. + /// + public DataModelComparison() : this(null) + { + } + + /// + /// Gets or sets whether the options below apply. Without them, new presets and comparison series start with the + /// provider of the whole app as their first model and without a judge. + /// + public bool PreselectOptions { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectOptions, false); + + /// + /// Gets or sets the minimum confidence which every model of a comparison needs, the judge included. + /// + public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE); + + /// + /// Gets or sets the model which every new preset and every new comparison series starts with as its first model, + /// e.g., the model the user works with every day and wants to test new models against. + /// + /// + /// NO_MODEL preselects none, and an empty text the provider which the whole app preselects. + /// + public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, NO_MODEL); + + /// + /// Gets or sets the judge which every new preset and every new comparison series starts with. A preselected + /// judge also switches the judge on. + /// + /// + /// NO_MODEL preselects none and leaves the judge off, and an empty text the provider which the whole app preselects. + /// + public string PreselectedJudgeProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedJudgeProvider, NO_MODEL); + + /// + /// Gets or sets the ID of the preset which the mode Answer Variance opens with, or an empty text for none. + /// + /// + /// Not managed: the presets belong to the user alone, as files of the data directory, so no organization can + /// know their IDs. + /// + public string PreselectedPresetId { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/AppIcons.cs b/app/MindWork AI Studio/Tools/AppIcons.cs index ffb3de2f8..2f81369a9 100644 --- a/app/MindWork AI Studio/Tools/AppIcons.cs +++ b/app/MindWork AI Studio/Tools/AppIcons.cs @@ -119,6 +119,9 @@ public static class AppIcons /// Marks the log viewer assistant. public const string ASSISTANT_LOG_VIEWER = LucideIcons.LOGS; + /// Marks the Model Comparison assistant. + public const string ASSISTANT_MODEL_COMPARISON = LucideIcons.GIT_COMPARE_ARROWS; + /// Marks the My Tasks assistant. public const string ASSISTANT_MY_TASKS = LucideIcons.LIST_TODO; @@ -185,6 +188,9 @@ public static class AppIcons /// Backs up the data of AI Studio. public const string BACKUP = LucideIcons.ARCHIVE_ARROW_DOWN; + /// Goes back from an opened item to the list it belongs to, such as from a comparison series to all series. + public const string BACK_TO_LIST = LucideIcons.ARROW_LEFT; + /// Marks something as blocked, such as a provider prohibited by the organization or a plugin below the required security level. public const string BLOCKED = LucideIcons.BAN; @@ -227,6 +233,12 @@ public static class AppIcons /// Clears an input, such as a search, a filter, or a shortcut. public const string CLEAR = LucideIcons.ERASER; + /// Marks a model which won clearly more than half of its decided model comparisons. + public const string CLEARLY_BETTER = LucideIcons.TRENDING_UP; + + /// Marks a model which won clearly less than half of its decided model comparisons. + public const string CLEARLY_WORSE = LucideIcons.TRENDING_DOWN; + /// Clears a chosen folder, such as the picture library of the slide assistant. public const string CLEAR_FOLDER = LucideIcons.FOLDER_X; @@ -242,6 +254,15 @@ public static class AppIcons /// Marks the name of a company, such as the employer in a job posting. public const string COMPANY = LucideIcons.BUILDING; + /// Shows the results of all model comparisons, with charts. + public const string COMPARISON_RESULTS = LucideIcons.CHART_COLUMN_STACKED; + + /// Stands for comparing how consistently two models answer the same request, in the Model Comparison assistant. + public const string COMPARISON_STABILITY = LucideIcons.REPEAT_2; + + /// Stands for comparing two models while working on real tasks with each of them, in the Model Comparison assistant. + public const string COMPARISON_TASKS = LucideIcons.BRIEFCASE_BUSINESS; + /// Marks the messages of a compiler that the coding assistant takes as input. public const string COMPILER_MESSAGES = LucideIcons.TRIANGLE_EXCLAMATION_POINT; @@ -347,6 +368,9 @@ public static class AppIcons /// Marks a document, such as a Markdown file. public const string DOCUMENT = LucideIcons.FILE_TEXT; + /// Ends the work with one model on a task, such as in a comparison series of the Model Comparison assistant. + public const string DONE_WITH_MODEL = LucideIcons.FLAG; + /// Downloads something from the internet, such as Pandoc or the content of a web page. public const string DOWNLOAD = LucideIcons.DOWNLOAD; @@ -587,6 +611,9 @@ public static class AppIcons /// Marks a text detail of a job posting, such as the mandatory information, the job description, the qualifications, or the responsibilities. public const string JOB_DETAILS = LucideIcons.CLIPBOARD_LIST; + /// Marks the model which judges the answers of other models, such as the judge of the Model Comparison assistant. + public const string JUDGE = LucideIcons.GAVEL; + /// Stands for a secret credential, such as an API key, a password, or an encryption secret. public const string KEY = LucideIcons.KEY_ROUND; @@ -740,6 +767,9 @@ public static class AppIcons /// Marks a step that has not started yet, such as a research of the BeyondFacts Lab. public const string NOT_STARTED = LucideIcons.CIRCLE_DASHED; + /// Marks a result which may still be chance, such as a model with too few decided model comparisons. + public const string NOT_YET_CLEAR = LucideIcons.HOURGLASS; + /// Removes the selected template, so that no template is used. public const string NO_TEMPLATE = LucideIcons.SQUARE_DASHED; @@ -812,6 +842,9 @@ public static class AppIcons /// Validates a generated assistant plugin before it is installed. public const string PLUGIN_VALIDATION = LucideIcons.FILE_CHECK; + /// Marks a saved set of inputs which can be loaded again, such as a preset of the Model Comparison assistant. + public const string PRESET = LucideIcons.BOOKMARK; + /// Shows a read-only view or a preview of an item, such as a profile or an assistant draft. public const string PREVIEW = LucideIcons.EYE; @@ -953,6 +986,9 @@ public static class AppIcons /// Stands for a runtime that AI Studio runs on, such as .NET or Tauri. public const string RUNTIME = LucideIcons.CPU; + /// Marks a run of a model comparison which got no vote, because a model did not answer or the vote was skipped. + public const string RUN_WITHOUT_VOTE = LucideIcons.CIRCLE_SLASH; + /// Saves something, such as a chat, a setting, or a plugin. public const string SAVE = LucideIcons.SAVE; @@ -1079,6 +1115,9 @@ public static class AppIcons /// Marks an on/off value, such as the switch component of an assistant plugin or a boolean value. public const string SWITCH = LucideIcons.TOGGLE_RIGHT; + /// Goes back to the choice between the modes of an assistant, such as those of the Model Comparison assistant. + public const string SWITCH_ASSISTANT_MODE = LucideIcons.ARROW_RIGHT_LEFT; + /// Marks a chat message from the system role, such as the system prompt. public const string SYSTEM_ROLE = LucideIcons.COG; @@ -1184,6 +1223,21 @@ public static class AppIcons /// Marks the transcription of speech into text, such as of a voice recording or a media file. public const string VOICE_TRANSCRIPTION = LucideIcons.AUDIO_LINES; + /// Marks a run of a model comparison which got your vote. + public const string VOTED = LucideIcons.VOTE; + + /// Marks the answer which lost a vote of a model comparison. + public const string VOTE_LOST = LucideIcons.CIRCLE_X; + + /// Marks a run of a model comparison which still waits for your vote. + public const string VOTE_MISSING = LucideIcons.CIRCLE_QUESTION_MARK; + + /// Marks two answers which a vote of a model comparison found equally good. + public const string VOTE_TIE = LucideIcons.CIRCLE_EQUAL; + + /// Marks the answer which won a vote of a model comparison. + public const string VOTE_WON = LucideIcons.CIRCLE_CHECK; + /// Signals a warning. public const string WARNING = LucideIcons.TRIANGLE_EXCLAMATION_POINT; diff --git a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs index fd2e8b689..1f0887a3f 100644 --- a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs @@ -70,6 +70,7 @@ public static class AssistantVisibilityExtensions Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT, Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT, Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT, + Components.MODEL_COMPARISON_ASSISTANT => ConfigurableAssistant.MODEL_COMPARISON_ASSISTANT, Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT, Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT, Components.BEYOND_FACTS_LAB_ASSISTANT => ConfigurableAssistant.BEYOND_FACTS_LAB_ASSISTANT, diff --git a/app/MindWork AI Studio/Tools/Components.cs b/app/MindWork AI Studio/Tools/Components.cs index c66605b03..102b673db 100644 --- a/app/MindWork AI Studio/Tools/Components.cs +++ b/app/MindWork AI Studio/Tools/Components.cs @@ -44,4 +44,5 @@ public enum Components VISUAL_BRIEFING_ASSISTANT, BATCH_PROCESSING_ASSISTANT, BEYOND_FACTS_LAB_ASSISTANT, + MODEL_COMPARISON_ASSISTANT, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index 980e7ff84..277268324 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -69,6 +69,7 @@ public static class ComponentsExtensions Components.I18N_ASSISTANT => false, Components.DOCUMENT_ANALYSIS_ASSISTANT => false, Components.BATCH_PROCESSING_ASSISTANT => false, + Components.MODEL_COMPARISON_ASSISTANT => false, Components.LOG_VIEWER_ASSISTANT => false, // The lab takes in nothing from other places, and its answers stay in its own steps: @@ -106,6 +107,7 @@ public static class ComponentsExtensions Components.I18N_ASSISTANT => TB("Localization Assistant"), Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"), Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"), + Components.MODEL_COMPARISON_ASSISTANT => TB("Model Comparison Assistant"), Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"), Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"), Components.META_ASSISTANT => TB("Assistant Builder"), @@ -174,6 +176,7 @@ public static class ComponentsExtensions // A policy-specific minimum is merged with this component default inside // the Batch Processing Assistant; the stricter level wins. Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default, + Components.MODEL_COMPARISON_ASSISTANT => settingsManager.ConfigurationData.ModelComparison.PreselectOptions ? settingsManager.ConfigurationData.ModelComparison.MinimumProviderConfidence : default, _ => default, }; @@ -208,6 +211,10 @@ public static class ComponentsExtensions Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE, + // The first model of a new comparison. The assistant resolves its options itself, see ModelComparisonPreselection, + // because the option may also preselect no model at all, which GetPreselectedProvider cannot express: + Components.MODEL_COMPARISON_ASSISTANT => settingsManager.ConfigurationData.ModelComparison.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ModelComparison.PreselectedProvider) : Settings.Provider.NONE, + // Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE, diff --git a/app/MindWork AI Studio/Tools/DataBackup/DataBackupContents.cs b/app/MindWork AI Studio/Tools/DataBackup/DataBackupContents.cs index 0b114b43e..e5a621974 100644 --- a/app/MindWork AI Studio/Tools/DataBackup/DataBackupContents.cs +++ b/app/MindWork AI Studio/Tools/DataBackup/DataBackupContents.cs @@ -46,6 +46,7 @@ public static class DataBackupContents new(DataBackupLocation.DATA_DIRECTORY, "tokenizers/", DataBackupEntryKind.TOKENIZERS), new(DataBackupLocation.DATA_DIRECTORY, "visualBriefings/", DataBackupEntryKind.VISUAL_BRIEFINGS), new(DataBackupLocation.DATA_DIRECTORY, "beyondFactsLab/", DataBackupEntryKind.BEYOND_FACTS_LAB), + new(DataBackupLocation.DATA_DIRECTORY, "modelComparisons/", DataBackupEntryKind.MODEL_COMPARISONS), new(DataBackupLocation.DATA_DIRECTORY, "databases/sqlite/", DataBackupEntryKind.DATA_SOURCE_INDEX), new(DataBackupLocation.DATA_DIRECTORY, "databases/vector_database/stores/", DataBackupEntryKind.VECTOR_STORES), new(DataBackupLocation.DATA_DIRECTORY, "mediaTranscripts/", DataBackupEntryKind.MEDIA_TRANSCRIPTS), diff --git a/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntry.cs b/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntry.cs index 05fba0ed1..041af99fa 100644 --- a/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntry.cs +++ b/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntry.cs @@ -19,6 +19,7 @@ public sealed record DataBackupEntry(DataBackupLocation Location, string Relativ or DataBackupEntryKind.TOKENIZERS or DataBackupEntryKind.VISUAL_BRIEFINGS or DataBackupEntryKind.BEYOND_FACTS_LAB + or DataBackupEntryKind.MODEL_COMPARISONS or DataBackupEntryKind.DATA_SOURCE_INDEX or DataBackupEntryKind.VECTOR_STORES or DataBackupEntryKind.MEDIA_TRANSCRIPTS; diff --git a/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntryKind.cs b/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntryKind.cs index 449bba26c..b24bd463f 100644 --- a/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntryKind.cs +++ b/app/MindWork AI Studio/Tools/DataBackup/DataBackupEntryKind.cs @@ -46,6 +46,11 @@ public enum DataBackupEntryKind /// BEYOND_FACTS_LAB, + /// + /// The presets, the results, and the comparison series with their chats of the Model Comparison assistant. + /// + MODEL_COMPARISONS, + /// /// The SQLite index of the data sources: which files are embedded, and their text chunks. /// diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 0fc24b761..35ededfbf 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -524,6 +524,12 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.AllowPromptOverrides, this.Id, settingsTable, dryRun); + // Config: Model Comparison defaults? + ManagedConfiguration.TryProcessConfiguration(x => x.ModelComparison, x => x.PreselectOptions, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.ModelComparison, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.ModelComparison, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); + ManagedConfiguration.TryProcessConfiguration(x => x.ModelComparison, x => x.PreselectedJudgeProvider, Guid.Empty, this.Id, settingsTable, dryRun); + // Config: Assistant Builder defaults? ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectOptions, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); diff --git a/app/MindWork AI Studio/Tools/Services/ProvenanceCleanupService.cs b/app/MindWork AI Studio/Tools/Services/ProvenanceCleanupService.cs index 6ef128e5d..34a8819bb 100644 --- a/app/MindWork AI Studio/Tools/Services/ProvenanceCleanupService.cs +++ b/app/MindWork AI Studio/Tools/Services/ProvenanceCleanupService.cs @@ -1,4 +1,5 @@ using AIStudio.Assistants.BeyondFactsLab; +using AIStudio.Assistants.ModelComparison; using AIStudio.Chat; using AIStudio.Settings; using AIStudio.Tools.DataBackup; @@ -11,10 +12,12 @@ namespace AIStudio.Tools.Services; /// One-shot startup service that removes from the provenance store what no stored chat refers to, see ProvenanceCleanup. /// /// -/// It knows the chats of the workspaces, the temporary chats, and the chats of the BeyondFacts Lab. It reads the -/// latter also while the lab is switched off: otherwise, they would lose what they received at every start. +/// It knows the chats of the workspaces, the temporary chats, the chats of the BeyondFacts Lab, and those of the +/// comparison series of the Model Comparison assistant. It reads the chats of the lab also while the lab is switched +/// off, and those of the series also while the assistant is hidden: otherwise, they would lose what they received at +/// every start. /// -public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClientProvider, DataRestoreService dataRestoreService, BeyondFactsLabStore labStore, ILogger logger) : BackgroundService +public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClientProvider, DataRestoreService dataRestoreService, BeyondFactsLabStore labStore, ModelComparisonStore comparisonStore, ILogger logger) : BackgroundService { /// /// When this run of the app started. Entries stored since belong to it and stay. @@ -103,6 +106,9 @@ public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClie foreach (var (chatId, labThread) in labStore.ReadAllThreadLocations()) locations[chatId] = StoredChatLocation.InLab(labThread); + foreach (var (chatId, seriesId) in comparisonStore.ReadAllSeriesChatLocations()) + locations[chatId] = StoredChatLocation.InComparisonSeries(seriesId); + return locations; } @@ -114,17 +120,19 @@ public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClie /// The chat, or null when it cannot be read. private async Task LoadChatAsync(StoredChatLocation location, Guid chatId) { - if (location.LabThread is not { } labThread) + if (location.LabThread is null && location.ComparisonSeriesId is null) return await WorkspaceBehaviour.LoadChatAsync(new(location.WorkspaceId, chatId)); try { - return await labStore.LoadThreadAsync(labThread); + return location.LabThread is { } labThread + ? await labStore.LoadThreadAsync(labThread) + : await comparisonStore.LoadSeriesChatAsync(location.ComparisonSeriesId!.Value, chatId); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) { // A chat which cannot be read keeps everything it received: - logger.LogDebug(exception, "The provenance startup cleanup could not read a chat of the BeyondFacts Lab."); + logger.LogDebug(exception, "The provenance startup cleanup could not read a chat of an assistant."); return null; } } @@ -135,7 +143,11 @@ public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClie /// The place of the chat. /// The ID of the chat. /// The time, or null when it is not known. - private DateTimeOffset? GetChatWrittenAt(StoredChatLocation location, Guid chatId) => location.LabThread is { } labThread - ? labStore.GetThreadWrittenAt(labThread) - : WorkspaceBehaviour.GetChatWrittenAt(location.WorkspaceId, chatId); + private DateTimeOffset? GetChatWrittenAt(StoredChatLocation location, Guid chatId) => location switch + { + { LabThread: { } labThread } => labStore.GetThreadWrittenAt(labThread), + { ComparisonSeriesId: { } seriesId } => comparisonStore.GetSeriesChatWrittenAt(seriesId, chatId), + + _ => WorkspaceBehaviour.GetChatWrittenAt(location.WorkspaceId, chatId), + }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/StoredChatLocation.cs b/app/MindWork AI Studio/Tools/Services/StoredChatLocation.cs index 7159418a2..835b53965 100644 --- a/app/MindWork AI Studio/Tools/Services/StoredChatLocation.cs +++ b/app/MindWork AI Studio/Tools/Services/StoredChatLocation.cs @@ -4,14 +4,15 @@ namespace AIStudio.Tools.Services; /// /// Where the cleanup of the provenance store at the start finds a chat: in a workspace, among the temporary chats, -/// or in the BeyondFacts Lab. +/// in the BeyondFacts Lab, or in a comparison series of the Model Comparison assistant. /// /// -/// Create it with InWorkspace or InLab. +/// Create it with InWorkspace, InLab, or InComparisonSeries. /// -/// The workspace of a chat outside the lab, Guid.Empty for a temporary chat. +/// The workspace of a chat in a workspace, Guid.Empty for a temporary chat or a chat of an assistant. /// Where a chat of the lab belongs, or null for any other chat. -public readonly record struct StoredChatLocation(Guid WorkspaceId, BflThreadLocation? LabThread) +/// The comparison series of a chat of the Model Comparison assistant, or null for any other chat. +public readonly record struct StoredChatLocation(Guid WorkspaceId, BflThreadLocation? LabThread, Guid? ComparisonSeriesId = null) { /// /// Gets the place of a chat in a workspace or among the temporary chats. @@ -24,4 +25,10 @@ public readonly record struct StoredChatLocation(Guid WorkspaceId, BflThreadLoca /// /// Where the chat belongs in the lab. public static StoredChatLocation InLab(BflThreadLocation thread) => new(Guid.Empty, thread); + + /// + /// Gets the place of a chat of a comparison series of the Model Comparison assistant. + /// + /// The ID of the series. + public static StoredChatLocation InComparisonSeries(Guid seriesId) => new(Guid.Empty, null, seriesId); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/SharedSelectionRules.cs b/app/MindWork AI Studio/Tools/SharedSelectionRules.cs new file mode 100644 index 000000000..472326fd1 --- /dev/null +++ b/app/MindWork AI Studio/Tools/SharedSelectionRules.cs @@ -0,0 +1,94 @@ +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools.ToolCallingSystem; + +namespace AIStudio.Tools; + +/// +/// Decides what a selection of tools or data sources offers when several models work with it, e.g., both models of a +/// comparison series of the Model Comparison assistant: each of them must be allowed everything the selection offers. +/// +/// +/// With a single model, each rule gives what that model alone gives, so a selection for one model stays as it was. +/// +public static class SharedSelectionRules +{ + /// + /// Gets the confidence which counts for several models: the lowest of them, since a tool which one of them may not + /// use must not be offered to any. + /// + /// The confidence of each model. + /// The lowest confidence, or ConfidenceLevel.NONE without any model. + public static ConfidenceLevel LowestConfidence(IEnumerable levels) => levels.DefaultIfEmpty(ConfidenceLevel.NONE).Min(); + + /// + /// Tells whether several models can use tools: only when each of them can. + /// + /// Whether each model can use tools. + /// The answer of the first model which cannot, with its reason, or that tools are available. + public static ToolCallingAvailability ToolCallingOfAll(IEnumerable availabilities) + { + foreach (var availability in availabilities) + if (!availability.IsAvailable) + return availability; + + return ToolCallingAvailability.Available(); + } + + /// + /// Decides how the data sources are searched when several models chat with the same data source options: by + /// Semantic Search only when each of them can search itself, so that none gets passages the other has to look for. + /// + /// How the data sources would be searched for each model on its own. + /// + /// The mode of the first model which cannot search itself, with its reason; otherwise every message when the user + /// prefers it, and Semantic Search when not. Without any model, every message, since nobody could search. + /// + public static EffectiveRetrievalMode RetrievalModeOfAll(IEnumerable modes) + { + EffectiveRetrievalMode? result = null; + foreach (var mode in modes) + { + if (mode.SemanticSearchBlockReason is not ToolOfferBlockReason.NONE) + return mode; + + if (result is null || mode.Mode is DataSourceRetrievalMode.EVERY_MESSAGE) + result = mode; + } + + return result ?? new(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.NONE); + } + + /// + /// Gets the data sources which several models may use: those which each of them may use. + /// + /// + /// Every list keeps only the data sources which the checks put into that list for each model, in the order of the + /// first model. That holds for those waiting for a re-index or a repair as well: such a data source passed every + /// check, and the reason belongs to the data source, so it lands in the same list for each model which may use it. + /// + /// What the checks found for each model. + /// The data sources each model may use, and nothing without any model. + public static AllowedSelectedDataSources DataSourcesOfAll(IReadOnlyList results) + { + if (results.Count == 0) + return new([], [], [], [], []); + + return new( + Shared(results, result => result.AllowedDataSources), + Shared(results, result => result.SelectedDataSources), + Shared(results, result => result.DataSourcesAwaitingReindex), + Shared(results, result => result.DataSourcesNeedingRepair), + Shared(results, result => result.UnreachableDataSources)); + } + + /// + /// Gets the data sources of one list which every result holds in that list, in the order of the first result. + /// + private static IReadOnlyList Shared(IReadOnlyList results, Func> list) + { + var otherIds = results.Skip(1).Select(result => list(result).Select(dataSource => dataSource.Id).ToHashSet(StringComparer.Ordinal)).ToList(); + return [.. list(results[0]).Where(dataSource => otherIds.All(ids => ids.Contains(dataSource.Id)))]; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs index 56baba540..745aa9223 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/SemanticSearch/SemanticSearchTool.cs @@ -86,11 +86,14 @@ public sealed class SemanticSearchTool(SettingsManager settingsManager, DataSour Id = ToolSelectionRules.SEMANTIC_SEARCH_TOOL_ID, ImplementationKey = ToolSelectionRules.SEMANTIC_SEARCH_TOOL_ID, - // Only a chat has data sources to search: + // + // Only a chat has data sources to search: the chat of the app, and the chats of a comparison series of the + // Model Comparison assistant. Its mode Answer Variance runs as the same component, but its chats keep + // the data sources switched off, see ModelComparisonProviderAdapter: + // VisibleIn = new() { - Chat = true, - Assistants = false, + AllowedComponents = [Components.CHAT, Components.MODEL_COMPARISON_ASSISTANT], }, Activation = ToolActivation.CONTEXT, diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.11.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.11.2.md index f8f4be1d7..ceda6d299 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.11.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.11.2.md @@ -3,8 +3,12 @@ - Added Lucide icons to assistant plugins: name an icon as `lucide:` followed by its name on lucide.dev, such as `lucide:house`. The names of MudBlazor icons keep working, so existing plugins need no change. - Added the choice whether AI Studio addresses you formally or informally, in languages which have both forms, such as German with "Sie" and "du". You find the option "Form of address" in the app settings, right below the language. The chats of the BeyondFacts Lab address you the same way. - Added the form of address to configuration plugins, so that organizations can preset or lock whether AI Studio addresses their users formally or informally. +- Added the Model Comparison assistant, which shows you which of two models helps you better. You compare them blindly, without knowing which model wrote which answer: with the same request sent to both, or while you work on your own tasks over days or weeks. Charts show you which model wins, and a judge model can give you a second opinion based on your own expectations. You find the assistant in the category "General". Thanks, Dominic Neuburg (`donework`), for this contribution. - Improved how well the AI remembers what it looked up in a chat: for its earlier answers, it now knows which web pages it read, which searches it ran, and which sources it used. When you ask a follow-up question, it can read the page again instead of claiming that it never read it. - Improved follow-up questions about web pages which a tool found, such as the results of a web search: the Read Web Page tool may read these pages again later in the same chat, also after you restart AI Studio and also when it may otherwise read only addresses which stand in the chat. - Improved the texts of AI Studio: we corrected grammar mistakes, missing words, and punctuation in many English texts and improved the wording of some German ones. A few texts which every language showed in English are now translated as well. +- Improved the descriptions at the top of all assistants: long words are now hyphenated, so that the justified text shows fewer large gaps. - Changed the icons of AI Studio to Lucide, an open-source icon set. All icons now share one style, and each feature shows the same icon wherever you meet it. -- Fixed assistant plugins which wrote numbers and dates according to the region settings of your computer. Plugins now show them the same way on every computer: a decimal number such as `12.5` keeps its point, and the `DateTime` helper writes the names of months in English. \ No newline at end of file +- Fixed assistant plugins which wrote numbers and dates according to the region settings of your computer. Plugins now show them the same way on every computer: a decimal number such as `12.5` keeps its point, and the `DateTime` helper writes the names of months in English. +- Fixed the translation, agenda, and e-mail assistants checking your input again and again in the background, about once a second, even while you weren't typing. +- Fixed the translation assistant complaining about a missing text before you had typed anything, e.g., right after you switched the live translation on or off. \ No newline at end of file diff --git a/app/Tests/Assistants/ChatSystemPromptOfAssistantsTests.cs b/app/Tests/Assistants/ChatSystemPromptOfAssistantsTests.cs index 877501a06..40902d7eb 100644 --- a/app/Tests/Assistants/ChatSystemPromptOfAssistantsTests.cs +++ b/app/Tests/Assistants/ChatSystemPromptOfAssistantsTests.cs @@ -16,6 +16,7 @@ using AIStudio.Assistants.I18N; using AIStudio.Assistants.IconFinder; using AIStudio.Assistants.JobPosting; using AIStudio.Assistants.LegalCheck; +using AIStudio.Assistants.ModelComparison; using AIStudio.Assistants.MyTasks; using AIStudio.Assistants.PromptOptimizer; using AIStudio.Assistants.RewriteImprove; @@ -61,6 +62,7 @@ public sealed class ChatSystemPromptOfAssistantsTests [typeof(AssistantIconFinder)] = ChatSystemPromptSource.ASSISTANT, [typeof(AssistantJobPostings)] = ChatSystemPromptSource.CUSTOM, [typeof(AssistantLegalCheck)] = ChatSystemPromptSource.ASSISTANT, + [typeof(AssistantModelComparison)] = ChatSystemPromptSource.AI_STUDIO_DEFAULT, [typeof(AssistantMyTasks)] = ChatSystemPromptSource.ASSISTANT, [typeof(AssistantPromptOptimizer)] = ChatSystemPromptSource.AI_STUDIO_DEFAULT, [typeof(AssistantRewriteImprove)] = ChatSystemPromptSource.CUSTOM, diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonBatchRunnerTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonBatchRunnerTests.cs new file mode 100644 index 000000000..a447f7e6c --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonBatchRunnerTests.cs @@ -0,0 +1,259 @@ +using System.Runtime.CompilerServices; + +using AIStudio.Assistants.ModelComparison; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how a batch makes several independent runs of the same comparison. +/// +/// +/// What is under test is that every run is genuinely independent -- its own request to both models, +/// its own presentation order -- not the single run itself, which ModelComparisonRunnerTests +/// already covers. +/// +[TestFixture] +public sealed class ModelComparisonBatchRunnerTests +{ + private static readonly ModelComparisonRequest REQUEST = new("The document.", "Summarize it."); + + [Test] + public async Task TheRequestedNumberOfRunsIsMade() + { + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + runCount: 5, + CancellationToken.None); + + var entries = await Task.WhenAll(tasks); + + Assert.That(entries, Has.Length.EqualTo(5)); + } + + [Test] + public void EveryRunStartsWithoutWaitingForAnyOfThemToFinish() + { + // Nothing here completes on its own; Start must still hand back its tasks right away. The + // cancellation afterward is only cleanup, so the never-ending calls do not outlive the test: + using var cancellation = new CancellationTokenSource(); + + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", (_, token) => Never(token)), + CreateParticipant("2", (_, token) => Never(token)), + REQUEST, + runCount: 3, + cancellation.Token); + + try + { + Assert.That(tasks, Has.Count.EqualTo(3)); + } + finally + { + cancellation.Cancel(); + } + } + + [Test] + public async Task NoMoreRunsThanTheLimitAskAModelAtOnce() + { + // Ten runs, twenty model calls between them; if every run started at once, every one of + // those twenty calls would be concurrent. A short delay gives the batch runner room to queue + // the rest behind the limit, which the peak below has to stay within -- two models per run: + var concurrentCalls = 0; + var peakConcurrentCalls = 0; + var peakLock = new object(); + + async IAsyncEnumerable Ask(string _, [EnumeratorCancellation] CancellationToken token) + { + var current = Interlocked.Increment(ref concurrentCalls); + lock (peakLock) + peakConcurrentCalls = Math.Max(peakConcurrentCalls, current); + + await Task.Delay(TimeSpan.FromMilliseconds(30), token); + Interlocked.Decrement(ref concurrentCalls); + yield return "An answer."; + } + + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", Ask), + CreateParticipant("2", Ask), + REQUEST, + runCount: 10, + CancellationToken.None); + + await Task.WhenAll(tasks); + + var expectedPeak = ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS * 2; + Assert.That(peakConcurrentCalls, Is.LessThanOrEqualTo(expectedPeak), $"At most {ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS} runs at a time, two model calls each."); + } + + [Test] + public async Task EachRunAsksBothModelsAfresh() + { + var firstAskedCount = 0; + var secondAskedCount = 0; + + IAsyncEnumerable AskFirst(string _, CancellationToken __) + { + Interlocked.Increment(ref firstAskedCount); + return Chunks("An answer."); + } + + IAsyncEnumerable AskSecond(string _, CancellationToken __) + { + Interlocked.Increment(ref secondAskedCount); + return Chunks("Another answer."); + } + + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", AskFirst), + CreateParticipant("2", AskSecond), + REQUEST, + runCount: 4, + CancellationToken.None); + + await Task.WhenAll(tasks); + + using (Assert.EnterMultipleScope()) + { + Assert.That(firstAskedCount, Is.EqualTo(4)); + Assert.That(secondAskedCount, Is.EqualTo(4)); + } + } + + [Test] + public async Task EveryRunCompletesOnItsOwnEvenWhenOneFails() + { + // Every third run fails; the rest must not be dragged down with it: + var callNumber = 0; + + IAsyncEnumerable Flaky(string _, CancellationToken __) + { + var thisCall = Interlocked.Increment(ref callNumber); + return thisCall % 3 == 0 ? Failing() : Chunks("An answer."); + } + + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", Flaky), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + runCount: 6, + CancellationToken.None); + + var entries = await Task.WhenAll(tasks); + + using (Assert.EnterMultipleScope()) + { + Assert.That(entries, Has.Length.EqualTo(6)); + Assert.That(entries.Count(entry => entry.Result.BothCompleted), Is.EqualTo(4)); + Assert.That(entries.Count(entry => !entry.Result.BothCompleted), Is.EqualTo(2)); + } + } + + [Test] + public void ARunCountBelowOneIsRefused() + { + var batchRunner = CreateBatchRunner(); + + Assert.That(() => batchRunner.Start( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + runCount: 0, + CancellationToken.None), Throws.TypeOf()); + } + + [Test] + public async Task BothPresentationOrdersCanComeUp() + { + // With enough runs, drawing only ever one of the two orders would be a bug, not bad luck: + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + runCount: 60, + CancellationToken.None); + + var entries = await Task.WhenAll(tasks); + var orders = entries.Select(entry => entry.PresentationOrder).Distinct().ToArray(); + + Assert.That(orders, Is.EquivalentTo(Enum.GetValues())); + } + + [Test] + public async Task WithAJudgeEveryRunHasItsOwnVerdict() + { + var judgeAskedCount = 0; + + IAsyncEnumerable AskJudge(string _, CancellationToken __) + { + Interlocked.Increment(ref judgeAskedCount); + return Chunks("""{"preferred": "A", "reasoning": "n/a"}"""); + } + + var tasks = CreateBatchRunner().Start( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + runCount: 3, + CancellationToken.None, + judge: CreateParticipant("judge", AskJudge)); + + var entries = await Task.WhenAll(tasks); + + using (Assert.EnterMultipleScope()) + { + Assert.That(judgeAskedCount, Is.EqualTo(3)); + Assert.That(entries.All(entry => entry.Result.Judge is { Completed: true }), Is.True); + } + } + + private static ModelComparisonBatchRunner CreateBatchRunner() => new(new ModelComparisonRunner(NullLogger.Instance, answers => Task.FromResult(answers))); + + private static ModelComparisonParticipant CreateParticipant(string id, Func> ask) => new() + { + Label = $"Model {id}", + Ask = ask, + }; + + private static async IAsyncEnumerable Chunks(params string[] chunks) + { + foreach (var chunk in chunks) + { + await Task.Yield(); + yield return chunk; + } + } + + private static async IAsyncEnumerable Never([EnumeratorCancellation] CancellationToken token) + { + try + { + await Task.Delay(Timeout.Infinite, token); + } + catch (OperationCanceledException) + { + // Expected once the test cleans up; there is nothing left to answer with. + } + + #pragma warning disable CS0162 // Unreachable code: the yield is what makes this an iterator + yield break; + #pragma warning restore CS0162 + } + + private static async IAsyncEnumerable Failing() + { + await Task.Yield(); + throw new InvalidOperationException("The model is not reachable."); + + #pragma warning disable CS0162 // Unreachable code: the yield is what makes this an iterator + // ReSharper disable once HeuristicUnreachableCode + yield break; + #pragma warning restore CS0162 + } +} diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonBatchTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonBatchTests.cs new file mode 100644 index 000000000..c55f882a1 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonBatchTests.cs @@ -0,0 +1,340 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the user goes through the runs of a batch, and how its votes and judge verdicts are counted. +/// +/// +/// The counts have to survive the presentation order changing from run to run: a model which won column A in one run +/// and column B in the next must still be counted as the same winner both times. +/// +[TestFixture] +public sealed class ModelComparisonBatchTests +{ + [Test] + public void ARunWaitsUntilTheUserIsThroughWithTheOnesBefore() + { + var batch = new ModelComparisonBatch(3); + batch.Add(Entry()); + batch.Add(Entry()); + + Assert.That(batch.CurrentEntry, Is.SameAs(batch.Entries[0])); + } + + [Test] + public void AVoteMovesOnToTheNextRun() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry()); + batch.Add(Entry()); + + var voteTaken = batch.Vote(ModelComparisonVote.COLUMN_A); + + using (Assert.EnterMultipleScope()) + { + Assert.That(voteTaken, Is.True); + Assert.That(batch.CurrentEntry, Is.SameAs(batch.Entries[1])); + Assert.That(batch.VoteChoiceAt(0), Is.EqualTo(ModelComparisonModelChoice.FIRST)); + Assert.That(batch.VoteChoiceAt(1), Is.Null, "The next run is still to be voted on."); + Assert.That(batch.IsLastEntry, Is.True); + } + } + + [Test] + public void ARunWithoutBothAnswersHasNothingToVoteOn() + { + var batch = new ModelComparisonBatch(1); + batch.Add(EntryWithoutAnAnswer()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batch.Vote(ModelComparisonVote.TIE), Is.False); + Assert.That(batch.CurrentIndex, Is.Zero); + Assert.That(batch.VoteChoiceAt(0), Is.Null); + } + } + + [Test] + public void ARunWithBothAnswersIsLeftOnlyByAVoteOrASkip() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batch.GoToNextEntry(), Is.False); + Assert.That(batch.CurrentIndex, Is.Zero); + } + } + + [Test] + public void ARunWithNothingToVoteOnCanBeLeftRightAway() + { + var batch = new ModelComparisonBatch(2); + batch.Add(EntryWithoutAnAnswer()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batch.GoToNextEntry(), Is.True); + Assert.That(batch.CurrentIndex, Is.EqualTo(1)); + } + } + + [Test] + public void TheLastRunEndsTheBatch() + { + var batch = new ModelComparisonBatch(1); + batch.Add(Entry()); + batch.Vote(ModelComparisonVote.TIE); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batch.IsFinished, Is.True); + Assert.That(batch.CurrentEntry, Is.Null); + } + } + + [Test] + public void SkippedVotesEndTheBatchOnceTheLastRunArrived() + { + var batch = new ModelComparisonBatch(3); + batch.Add(Entry()); + + batch.SkipRemainingVotes(runsStillArriving: true); + var finishedWhileRunsArrive = batch.IsFinished; + + batch.Add(Entry()); + batch.Add(Entry()); + batch.Settle(canceled: false); + + using (Assert.EnterMultipleScope()) + { + Assert.That(finishedWhileRunsArrive, Is.False); + Assert.That(batch.IsFinished, Is.True); + } + } + + [Test] + public void SkippedVotesEndASettledBatchRightAway() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry()); + batch.Add(Entry()); + + batch.SkipRemainingVotes(runsStillArriving: false); + + Assert.That(batch.IsFinished, Is.True); + } + + [Test] + public void AStoppedBatchGoesStraightToTheSummary() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry()); + batch.Add(EntryWithoutAnAnswer()); + + batch.Settle(canceled: true); + + Assert.That(batch.IsFinished, Is.True); + } + + [Test] + public void ABatchWhichWasNeitherStoppedNorSkippedWaitsForTheVotes() + { + var batch = new ModelComparisonBatch(1); + batch.Add(Entry()); + + batch.Settle(canceled: false); + + Assert.That(batch.IsFinished, Is.False); + } + + [Test] + public void TheStatusFollowsTheRun() + { + // Two runs arrived, the next ones ask the models, and the rest wait for a free slot: + var batch = new ModelComparisonBatch(2 + ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS + 1); + batch.Add(Entry()); + batch.Add(EntryWithoutAnAnswer()); + + var statusBeforeTheVote = batch.StatusOf(0); + batch.Vote(ModelComparisonVote.COLUMN_A); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statusBeforeTheVote, Is.EqualTo(ModelComparisonRunStatus.AWAITING_VOTE)); + Assert.That(batch.StatusOf(0), Is.EqualTo(ModelComparisonRunStatus.VOTED)); + Assert.That(batch.StatusOf(1), Is.EqualTo(ModelComparisonRunStatus.NO_USABLE_ANSWER)); + for (var index = 2; index < 2 + ModelComparisonBatchRunner.MAX_CONCURRENT_RUNS; index++) + Assert.That(batch.StatusOf(index), Is.EqualTo(ModelComparisonRunStatus.RUNNING), $"Run {index + 1} asks the models."); + + Assert.That(batch.StatusOf(batch.RunCount - 1), Is.EqualTo(ModelComparisonRunStatus.QUEUED)); + } + } + + [Test] + public void ARunTheUserMovedPastWithoutAVoteIsSkipped() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry()); + batch.Add(Entry()); + + batch.SkipThisRun(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batch.StatusOf(0), Is.EqualTo(ModelComparisonRunStatus.VOTE_SKIPPED)); + Assert.That(batch.StatusOf(1), Is.EqualTo(ModelComparisonRunStatus.AWAITING_VOTE)); + } + } + + [Test] + public void VotesAreCountedByModelNotByColumn() + { + // The first model stands in column A twice and in column B once, and the user votes for it every time: + var batch = new ModelComparisonBatch(3); + VoteOn(batch, Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST), ModelComparisonVote.COLUMN_A); + VoteOn(batch, Entry(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST), ModelComparisonVote.COLUMN_B); + VoteOn(batch, Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST), ModelComparisonVote.COLUMN_A); + + var tally = batch.TallyVotes(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tally.TotalRunCount, Is.EqualTo(3)); + Assert.That(tally.FirstModelWins, Is.EqualTo(3)); + Assert.That(tally.SecondModelWins, Is.Zero); + } + } + + [Test] + public void ARunWithoutAVoteIsNotCounted() + { + var batch = new ModelComparisonBatch(2); + VoteOn(batch, Entry(), ModelComparisonVote.COLUMN_A); + batch.Add(Entry()); + batch.SkipThisRun(); + + var tally = batch.TallyVotes(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tally.TotalRunCount, Is.EqualTo(2), "Both runs happened."); + Assert.That(tally.ScoredRunCount, Is.EqualTo(1), "Only one run was voted on."); + Assert.That(tally.FirstModelWins, Is.EqualTo(1)); + } + } + + [Test] + public void JudgeVerdictsAreCountedByModelNotByColumn() + { + // The first model stands in column A twice and in column B once, and the judge prefers it every time: + var batch = new ModelComparisonBatch(3); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, Verdict(ModelComparisonVote.COLUMN_A))); + batch.Add(Entry(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, Verdict(ModelComparisonVote.COLUMN_B))); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, Verdict(ModelComparisonVote.COLUMN_A))); + + var tally = batch.TallyJudgeVerdicts(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batch.JudgeChoiceAt(1), Is.EqualTo(ModelComparisonModelChoice.FIRST), "Column B of the second run is the first model."); + Assert.That(tally.ScoredRunCount, Is.EqualTo(3)); + Assert.That(tally.FirstModelWins, Is.EqualTo(3)); + Assert.That(tally.SecondModelWins, Is.Zero); + } + } + + [Test] + public void AJudgeTieIsATieInEitherOrder() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, Verdict(ModelComparisonVote.TIE))); + batch.Add(Entry(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, Verdict(ModelComparisonVote.TIE))); + + Assert.That(batch.TallyJudgeVerdicts().Ties, Is.EqualTo(2)); + } + + [Test] + public void ARunWithoutAUsableJudgeVerdictIsNotCounted() + { + var batch = new ModelComparisonBatch(2); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, Verdict(ModelComparisonVote.COLUMN_A))); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, new ModelComparisonJudgeVerdict { Label = "Judge", Completed = false })); + + var tally = batch.TallyJudgeVerdicts(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tally.TotalRunCount, Is.EqualTo(2), "Both runs happened."); + Assert.That(tally.ScoredRunCount, Is.EqualTo(1), "Only one run had a usable verdict."); + Assert.That(batch.HasJudgeVerdict(0), Is.True); + Assert.That(batch.HasJudgeVerdict(1), Is.False); + } + } + + [Test] + public void ACopyDoesNotChangeWithTheBatch() + { + var batch = new ModelComparisonBatch(3); + batch.Add(Entry()); + batch.SkipRemainingVotes(runsStillArriving: true); + + var copy = batch.Copy(); + batch.Vote(ModelComparisonVote.COLUMN_A); + batch.Add(Entry()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.RunCount, Is.EqualTo(3)); + Assert.That(copy.Entries, Has.Count.EqualTo(1)); + Assert.That(copy.CurrentIndex, Is.Zero); + Assert.That(copy.VoteChoiceAt(0), Is.Null); + Assert.That(copy.SkipsRemainingVotes, Is.True); + } + } + + private static void VoteOn(ModelComparisonBatch batch, ModelComparisonBatchEntry entry, ModelComparisonVote vote) + { + batch.Add(entry); + batch.Vote(vote); + } + + private static ModelComparisonBatchEntry Entry() => Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST); + + private static ModelComparisonBatchEntry Entry(ModelComparisonPresentationOrder order, ModelComparisonJudgeVerdict? judge = null) => new() + { + PresentationOrder = order, + Result = new ModelComparisonRunResult + { + First = Answer("First", true), + Second = Answer("Second", true), + Judge = judge, + }, + }; + + private static ModelComparisonBatchEntry EntryWithoutAnAnswer() => new() + { + PresentationOrder = ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, + Result = new ModelComparisonRunResult + { + First = Answer("First", true), + Second = Answer("Second", false), + }, + }; + + private static ModelComparisonJudgeVerdict Verdict(ModelComparisonVote preferred) => new() + { + Label = "Judge", + Completed = true, + Preferred = preferred, + }; + + private static ModelComparisonAnswer Answer(string label, bool completed) => new() + { + Label = label, + Text = completed ? $"The answer of {label}." : string.Empty, + Completed = completed, + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonChartsTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonChartsTests.cs new file mode 100644 index 000000000..354c109e1 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonChartsTests.cs @@ -0,0 +1,25 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks what the charts of the results have in common. +/// +[TestFixture] +public sealed class ModelComparisonChartsTests +{ + [TestCase(new double[0], 1)] + [TestCase(new[] { 0.0 }, 1)] + [TestCase(new[] { 5.0 }, 1)] + [TestCase(new[] { 6.0 }, 2)] + [TestCase(new[] { 3.0, 10.0 }, 2)] + [TestCase(new[] { 11.0 }, 5)] + [TestCase(new[] { 26.0 }, 10)] + [TestCase(new[] { 0.4, 3000.0 }, 1000)] + public void TheAxisStepsByOneTwoOrFiveTimesAPowerOfTen(double[] values, int expectedStep) => Assert.That(ModelComparisonCharts.AxisStep(values), Is.EqualTo(expectedStep)); + + [TestCase(1, 0)] + [TestCase(4, 0)] + [TestCase(5, 30)] + public void OnlyManyModelsTiltTheirNames(int modelCount, int expectedRotation) => Assert.That(ModelComparisonCharts.LabelRotation(modelCount), Is.EqualTo(expectedRotation)); +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeExpectationsTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeExpectationsTests.cs new file mode 100644 index 000000000..14d492fb0 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeExpectationsTests.cs @@ -0,0 +1,31 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks that a judge starts only by the expectations, values, and goals of the user, or with the confirmation that it +/// may decide by its own standards. +/// +[TestFixture] +public sealed class ModelComparisonJudgeExpectationsTests +{ + [TestCase("", false, true)] + [TestCase(" ", false, true)] + [TestCase("", true, false)] + [TestCase("I prefer short answers.", false, false)] + [TestCase("I prefer short answers.", true, false)] + public void WithoutExpectationsTheUserHasToConfirm(string expectations, bool ownStandardsAccepted, bool expectedIssue) + { + Assert.That(ModelComparisonJudgeExpectations.IssueOf(expectations, ownStandardsAccepted) is not null, Is.EqualTo(expectedIssue)); + } + + [Test] + public void ThePromptOfAJudgeGetsTheExpectationsOnlyWhenThereAreSome() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonJudgeFormat.ExpectationsOfTheUser(" "), Is.Empty); + Assert.That(ModelComparisonJudgeFormat.ExpectationsOfTheUser(" I prefer short answers. "), Does.EndWith("# Expectations, values, and goals of the user\nI prefer short answers.")); + } + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeFormatTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeFormatTests.cs new file mode 100644 index 000000000..c9c4230e4 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeFormatTests.cs @@ -0,0 +1,97 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks that both judges of the Model Comparison assistant answer alike, each with the names the user sees. +/// +[TestFixture] +public sealed class ModelComparisonJudgeFormatTests +{ + [TestCase("A", ModelComparisonVote.COLUMN_A)] + [TestCase(" b ", ModelComparisonVote.COLUMN_B)] + [TestCase("tie", ModelComparisonVote.TIE)] + [TestCase("1", null)] + [TestCase(null, null)] + public void TheJudgeOfAnswersNamesThemByLetter(string? preferred, ModelComparisonVote? expected) => Assert.That(ModelComparisonJudgeFormat.ANSWERS.VoteOf(preferred), Is.EqualTo(expected)); + + [TestCase("1", ModelComparisonVote.COLUMN_A)] + [TestCase("2", ModelComparisonVote.COLUMN_B)] + [TestCase("TIE", ModelComparisonVote.TIE)] + [TestCase("A", null)] + [TestCase("3", null)] + public void TheJudgeOfChatsNamesThemByNumber(string? preferred, ModelComparisonVote? expected) => Assert.That(ModelComparisonJudgeFormat.CHATS.VoteOf(preferred), Is.EqualTo(expected)); + + [Test] + public void AVerdictOnChatsIsReadWithTheirNumbers() + { + var verdict = ModelComparisonJudgeVerdict.Parse("judge", true, """{"preferred": "1", "reasoning": "Chat 1 was right."}""", ModelComparisonJudgeFormat.CHATS); + var verdictWithLetter = ModelComparisonJudgeVerdict.Parse("judge", true, """{"preferred": "A", "reasoning": "n/a"}""", ModelComparisonJudgeFormat.CHATS); + + using (Assert.EnterMultipleScope()) + { + Assert.That(verdict.Completed, Is.True); + Assert.That(verdict.Preferred, Is.EqualTo(ModelComparisonVote.COLUMN_A)); + Assert.That(verdict.Reasoning, Is.EqualTo("Chat 1 was right.")); + Assert.That(verdictWithLetter.Completed, Is.False); + } + } + + [Test] + public void BothJudgesGetTheSameShapeOfAnswer() + { + var answers = ModelComparisonJudgeFormat.ANSWERS.AnswerInstructions(string.Empty); + var chats = ModelComparisonJudgeFormat.CHATS.AnswerInstructions("German"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(answers, Does.Contain("""{"preferred": "A", "reasoning": """)); + Assert.That(answers, Does.Contain("""exactly one of "A", "B", or "TIE".""")); + Assert.That(answers, Does.Contain("refer to each answer only by its letter, A or B")); + Assert.That(answers, Does.Not.Contain("Write the reasoning in")); + + Assert.That(chats, Does.Contain("""{"preferred": "1", "reasoning": """)); + Assert.That(chats, Does.Contain("""exactly one of "1", "2", or "TIE".""")); + Assert.That(chats, Does.Contain("refer to each chat only by its number, 1 or 2")); + Assert.That(chats, Does.Contain("Write the reasoning in German")); + } + } + + [Test] + public void TheJudgeOfChatsReadsBothChats() + { + // The prompt is a raw string literal, which keeps the line endings of the checkout: + var prompt = new ModelComparisonChatJudgeRequest("[User]\nfirst", "[User]\nsecond", string.Empty, string.Empty).ToPrompt().ReplaceLineEndings("\n"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompt, Does.Contain("# Chat 1\n[User]\nfirst")); + Assert.That(prompt, Does.Contain("# Chat 2\n[User]\nsecond")); + Assert.That(prompt, Does.Contain("must not guess, which model answered in which chat")); + Assert.That(prompt, Does.Not.Contain("Expectations, values, and goals of the user")); + Assert.That(prompt, Does.EndWith("not forced into English.")); + } + } + + [Test] + public void TheJudgeOfChatsDecidesByTheExpectationsOfTheUser() + { + var prompt = new ModelComparisonChatJudgeRequest("[User]\nfirst", "[User]\nsecond", string.Empty, "I prefer short answers.").ToPrompt().ReplaceLineEndings("\n"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompt, Does.Contain("# Expectations, values, and goals of the user\nI prefer short answers.\n\n# Chat 1"), "The standards of the user stand before the chats."); + Assert.That(prompt, Does.Contain("before any preference of your own")); + } + } + + [Test] + public void ACacheBusterIsAnIgnorableLastLine() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonJudgeFormat.WithCacheBuster("prompt", string.Empty), Is.EqualTo("prompt")); + Assert.That(ModelComparisonJudgeFormat.WithCacheBuster("prompt", "abc"), Does.StartWith("prompt\n\n(Request ID: abc.")); + } + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeRequestTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeRequestTests.cs new file mode 100644 index 000000000..cbf5ead44 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeRequestTests.cs @@ -0,0 +1,103 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks the prompt the judge is asked with. +/// +[TestFixture] +public sealed class ModelComparisonJudgeRequestTests +{ + private static readonly ModelComparisonRequest REQUEST = new("The document.", "Summarize it."); + + [Test] + public void ThePromptCarriesTheExpectationsOfTheUserAndBothAnswers() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, "I prefer short answers.", "Answer from the first model.", "Answer from the second model."); + + var prompt = judgeRequest.ToPrompt(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompt, Does.Contain("# Expectations, values, and goals of the user\nI prefer short answers.")); + Assert.That(prompt, Does.Contain("before any preference of your own"), "The standards of the user come first."); + Assert.That(prompt, Does.Contain("Answer from the first model.")); + Assert.That(prompt, Does.Contain("Answer from the second model.")); + Assert.That(prompt, Does.Contain(REQUEST.ToPrompt(headingLevel: 2)), "The request stands below the heading of the judge prompt."); + Assert.That(prompt.ReplaceLineEndings("\n"), Does.Not.Contain("\n# Material"), "The headings of the request must not stand on the level of the answers."); + } + } + + [Test] + public void WhiteSpaceAroundTheInstructionsIsDropped() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, " Keep it short. \n", "First.", "Second."); + + Assert.That(judgeRequest.Instructions, Is.EqualTo("Keep it short.")); + } + + [Test] + public void WithoutExpectationsThePromptCarriesTheGenericCriterionAlone() + { + // The user confirmed that the judge may decide by its own standards: + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, string.Empty, "Answer from the first model.", "Answer from the second model."); + + var prompt = judgeRequest.ToPrompt(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompt, Does.Contain("better answers the question or better completes the task")); + Assert.That(prompt, Does.Contain("Answer from the first model.")); + Assert.That(prompt, Does.Contain("Answer from the second model.")); + Assert.That(prompt, Does.Not.Contain("Expectations, values, and goals of the user")); + } + } + + [Test] + public void WithoutALanguageNameThePromptAsksForNoParticularOne() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, string.Empty, "First.", "Second."); + + Assert.That(judgeRequest.ToPrompt(), Does.Not.Contain("Write the reasoning in")); + } + + [Test] + public void ALanguageNameTellsTheJudgeWhatToWriteIn() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, string.Empty, "First.", "Second.", "German"); + + Assert.That(judgeRequest.ToPrompt(), Does.Contain("Write the reasoning in German")); + } + + [Test] + public void TheAnswersAreLabeledAAndBNotByModelOrder() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, string.Empty, "Answer from the first model.", "Answer from the second model."); + + var prompt = judgeRequest.ToPrompt(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompt, Does.Contain("# Answer A")); + Assert.That(prompt, Does.Contain("# Answer B")); + Assert.That(prompt, Does.Not.Contain("# Answer 1")); + Assert.That(prompt, Does.Not.Contain("# Answer 2")); + } + } + + [Test] + public void WithoutACacheBusterThePromptIsUnchanged() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, string.Empty, "First.", "Second."); + + Assert.That(judgeRequest.ToPrompt(), Is.EqualTo(judgeRequest.ToPrompt(string.Empty))); + } + + [Test] + public void TwoDifferentCacheBustersBuildDifferentPrompts() + { + var judgeRequest = new ModelComparisonJudgeRequest(REQUEST, string.Empty, "First.", "Second."); + + Assert.That(judgeRequest.ToPrompt("one"), Is.Not.EqualTo(judgeRequest.ToPrompt("two"))); + } +} diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeVerdictTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeVerdictTests.cs new file mode 100644 index 000000000..77bf7614a --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonJudgeVerdictTests.cs @@ -0,0 +1,79 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the judge's raw answer is turned into a verdict. +/// +/// +/// The judge is asked for one JSON object and nothing else, but a model rarely holds to that +/// perfectly. Parsing has to look past a stray sentence around the object, and has to end up with a +/// verdict which is not ModelComparisonJudgeVerdict.Completed rather than throw when +/// there is nothing usable to read. +/// +[TestFixture] +public sealed class ModelComparisonJudgeVerdictTests +{ + [Test] + public void APlainJsonObjectIsRead() + { + var verdict = ModelComparisonJudgeVerdict.Parse("Judge", true, """{"preferred": "A", "reasoning": "It kept every number from the document."}"""); + + using (Assert.EnterMultipleScope()) + { + Assert.That(verdict.Completed, Is.True); + Assert.That(verdict.Label, Is.EqualTo("Judge")); + Assert.That(verdict.Preferred, Is.EqualTo(ModelComparisonVote.COLUMN_A)); + Assert.That(verdict.Reasoning, Is.EqualTo("It kept every number from the document.")); + } + } + + [Test] + public void APreferenceIsReadWithoutRegardToCase() + { + var verdict = ModelComparisonJudgeVerdict.Parse("Judge", true, """{"preferred": "tie", "reasoning": "Both cover the same points."}"""); + + Assert.That(verdict.Preferred, Is.EqualTo(ModelComparisonVote.TIE)); + } + + [Test] + public void TextAroundTheJsonObjectIsIgnored() + { + var verdict = ModelComparisonJudgeVerdict.Parse("Judge", true, """ + Sure, here is my verdict: + ```json + {"preferred": "B", "reasoning": "It is shorter and just as complete."} + ``` + """); + + using (Assert.EnterMultipleScope()) + { + Assert.That(verdict.Completed, Is.True); + Assert.That(verdict.Preferred, Is.EqualTo(ModelComparisonVote.COLUMN_B)); + } + } + + [Test] + public void AJudgeWhichDidNotAnswerIsNotCompleted() + { + var verdict = ModelComparisonJudgeVerdict.Parse("Judge", false, string.Empty); + + using (Assert.EnterMultipleScope()) + { + Assert.That(verdict.Completed, Is.False); + Assert.That(verdict.Preferred, Is.Null); + Assert.That(verdict.Reasoning, Is.Empty); + } + } + + [TestCase("Sorry, I cannot help with that.")] + [TestCase("""{"reasoning": "Missing the preference field."}""")] + [TestCase("""{"preferred": "C", "reasoning": "Not one of the three allowed values."}""")] + [TestCase("""{"preferred": "A", "reasoning": }""")] + public void AnUnreadableAnswerIsNotCompleted(string judgeText) + { + var verdict = ModelComparisonJudgeVerdict.Parse("Judge", true, judgeText); + + Assert.That(verdict.Completed, Is.False); + } +} diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonNamesTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonNamesTests.cs new file mode 100644 index 000000000..27a865dd7 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonNamesTests.cs @@ -0,0 +1,93 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks that presets and comparison series keep names which the user can tell apart. +/// +[TestFixture] +public sealed class ModelComparisonNamesTests +{ + private const int MAX_LENGTH = 60; + + [TestCase("Weekly reports", true)] + [TestCase("weekly REPORTS", true)] + [TestCase(" Weekly reports ", true)] + [TestCase("Weekly report", false)] + public void ANameIsTakenRegardlessOfCaseAndSpaces(string name, bool expectedTaken) + { + Assert.That(ModelComparisonNames.IsTaken(name, ["Weekly reports", "Daily mails"]), Is.EqualTo(expectedTaken)); + } + + [Test] + public void ATakenNameWithSpacesAroundItCounts() + { + Assert.That(ModelComparisonNames.IsTaken("Weekly reports", [" Weekly reports "]), Is.True); + } + + [Test] + public void AFreeNameStaysAsItIs() + { + Assert.That(ModelComparisonNames.FreeNameFrom(" Weekly reports ", ["Daily mails"], MAX_LENGTH), Is.EqualTo("Weekly reports")); + } + + [Test] + public void ATakenNameGetsTheNextFreeNumber() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonNames.FreeNameFrom("Weekly reports", ["Weekly reports"], MAX_LENGTH), Is.EqualTo("Weekly reports (2)")); + Assert.That(ModelComparisonNames.FreeNameFrom("Weekly reports", ["Weekly reports", "weekly reports (2)"], MAX_LENGTH), Is.EqualTo("Weekly reports (3)")); + } + } + + [Test] + public void ANumberedNameCountsOnFromItsBase() + { + string[] takenNames = ["Weekly reports", "Weekly reports (2)"]; + Assert.That(ModelComparisonNames.FreeNameFrom("Weekly reports (2)", takenNames, MAX_LENGTH), Is.EqualTo("Weekly reports (3)"), "A copy of a copy must not become \"Weekly reports (2) (2)\"."); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void ASeriesNeedsAName(string? name) + { + Assert.That(ModelComparisonNames.SeriesNameIssue(name, []), Is.Not.Null); + } + + [Test] + public void ASeriesNameMayNotBeTooLong() + { + var longestName = new string('a', ModelComparisonNames.MAX_SERIES_NAME_LENGTH); + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonNames.SeriesNameIssue(longestName, []), Is.Null); + Assert.That(ModelComparisonNames.SeriesNameIssue($" {longestName} ", []), Is.Null, "Spaces around the name do not count, since they are not stored."); + Assert.That(ModelComparisonNames.SeriesNameIssue(longestName + "a", []), Is.Not.Null); + } + } + + [Test] + public void ASeriesNameMayNotBeTakenByAnotherSeries() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonNames.SeriesNameIssue("weekly REPORTS", ["Weekly reports"]), Is.Not.Null); + Assert.That(ModelComparisonNames.SeriesNameIssue("Weekly reports", ["Daily mails"]), Is.Null); + } + } + + [Test] + public void ALongNameIsShortenedForItsNumber() + { + var longName = new string('a', MAX_LENGTH); + var freeName = ModelComparisonNames.FreeNameFrom(longName, [longName], MAX_LENGTH); + + using (Assert.EnterMultipleScope()) + { + Assert.That(freeName, Has.Length.EqualTo(MAX_LENGTH)); + Assert.That(freeName, Does.EndWith(" (2)")); + } + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonOutcomeTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonOutcomeTests.cs new file mode 100644 index 000000000..695aa85c0 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonOutcomeTests.cs @@ -0,0 +1,74 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Tools; + +using MudBlazor; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the summary of a batch shows what the user and the judge chose. +/// +[TestFixture] +public sealed class ModelComparisonOutcomeTests +{ + [Test] + public void WithoutAVoteTheCardOnlyFillsItsColumn() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonOutcome.CardClassOf(null, ModelComparisonModelChoice.FIRST), Is.EqualTo(ModelComparisonOutcome.FILL_THE_COLUMN)); + Assert.That(ModelComparisonOutcome.ColorOf(null, ModelComparisonModelChoice.FIRST), Is.EqualTo(Color.Default)); + Assert.That(ModelComparisonOutcome.IconOf(null, ModelComparisonModelChoice.FIRST), Is.EqualTo(AppIcons.VOTE_MISSING)); + } + } + + [TestCase(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.FIRST, Color.Success, "mud-border-success")] + [TestCase(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.SECOND, Color.Error, "mud-border-error")] + [TestCase(ModelComparisonModelChoice.TIE, ModelComparisonModelChoice.SECOND, Color.Warning, "mud-border-warning")] + public void TheVoteColorsTheCardOfEachModel(ModelComparisonModelChoice vote, ModelComparisonModelChoice model, Color expectedColor, string expectedBorder) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonOutcome.ColorOf(vote, model), Is.EqualTo(expectedColor)); + Assert.That(ModelComparisonOutcome.CardClassOf(vote, model), Does.StartWith(ModelComparisonOutcome.FILL_THE_COLUMN).And.EndWith(expectedBorder)); + } + } + + [Test] + public void EveryOutcomeHasAnIconOfItsOwn() + { + var icons = new[] + { + ModelComparisonOutcome.IconOf(null, ModelComparisonModelChoice.FIRST), + ModelComparisonOutcome.IconOf(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.FIRST), + ModelComparisonOutcome.IconOf(ModelComparisonModelChoice.SECOND, ModelComparisonModelChoice.FIRST), + ModelComparisonOutcome.IconOf(ModelComparisonModelChoice.TIE, ModelComparisonModelChoice.FIRST), + }; + + Assert.That(icons, Is.Unique); + } + + [TestCase(null, ModelComparisonModelChoice.FIRST, false)] + [TestCase(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.FIRST, true)] + [TestCase(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.SECOND, false)] + [TestCase(ModelComparisonModelChoice.TIE, ModelComparisonModelChoice.FIRST, true)] + [TestCase(ModelComparisonModelChoice.TIE, ModelComparisonModelChoice.SECOND, true)] + public void TheJudgeFavorsItsChoiceOrBothOnATie(ModelComparisonModelChoice? judgeChoice, ModelComparisonModelChoice model, bool expected) => Assert.That(ModelComparisonOutcome.JudgeFavors(judgeChoice, model), Is.EqualTo(expected)); + + [Test] + public void ARunGivesTheAnswerOfEachModel() + { + var result = new ModelComparisonRunResult + { + First = new() { Label = "first", Text = "one", Completed = true }, + Second = new() { Label = "second", Text = "two", Completed = true }, + }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.AnswerOf(ModelComparisonModelChoice.FIRST), Is.SameAs(result.First)); + Assert.That(result.AnswerOf(ModelComparisonModelChoice.SECOND), Is.SameAs(result.Second)); + Assert.Throws(() => result.AnswerOf(ModelComparisonModelChoice.TIE)); + } + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonPreselectionTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonPreselectionTests.cs new file mode 100644 index 000000000..8fe14c2d4 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonPreselectionTests.cs @@ -0,0 +1,134 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks which models a new preset or comparison series of the Model Comparison assistant starts with. +/// +/// +/// Every test configures at least two providers: with exactly one, GetPreselectedProvider takes a shortcut and +/// returns it without looking at any preselection, so a test of the fallback would pass for the wrong reason. +/// +[TestFixture] +public sealed class ModelComparisonPreselectionTests +{ + private const string DAILY_PROVIDER_ID = "11111111-1111-1111-1111-111111111111"; + private const string JUDGE_PROVIDER_ID = "22222222-2222-2222-2222-222222222222"; + private const string APP_DEFAULT_PROVIDER_ID = "33333333-3333-3333-3333-333333333333"; + + [Test] + public void WithoutOptionsTheFirstModelIsTheAppDefaultAndThereIsNoJudge() + { + var settingsManager = CreateSettingsManager(); + settingsManager.ConfigurationData.ModelComparison.PreselectedProvider = DAILY_PROVIDER_ID; + settingsManager.ConfigurationData.ModelComparison.PreselectedJudgeProvider = JUDGE_PROVIDER_ID; + + using (Assert.EnterMultipleScope()) + { + Assert.That(settingsManager.PreselectedComparisonModel.Id, Is.EqualTo(APP_DEFAULT_PROVIDER_ID), "With the options switched off, the stored models must not count."); + Assert.That(settingsManager.PreselectedComparisonJudge, Is.EqualTo(AIStudio.Settings.Provider.NONE), "Without the options, a new comparison starts without a judge."); + } + } + + [Test] + public void TheDefaultOptionsPreselectNoModel() + { + var settingsManager = CreateSettingsManager(); + settingsManager.ConfigurationData.ModelComparison.PreselectOptions = true; + + using (Assert.EnterMultipleScope()) + { + Assert.That(settingsManager.ConfigurationData.ModelComparison.PreselectedProvider, Is.EqualTo(DataModelComparison.NO_MODEL)); + Assert.That(settingsManager.ConfigurationData.ModelComparison.PreselectedJudgeProvider, Is.EqualTo(DataModelComparison.NO_MODEL)); + Assert.That(settingsManager.PreselectedComparisonModel, Is.EqualTo(AIStudio.Settings.Provider.NONE), "\"No model preselected\" must not fall back to the provider of the whole app."); + Assert.That(settingsManager.PreselectedComparisonJudge, Is.EqualTo(AIStudio.Settings.Provider.NONE)); + } + } + + [Test] + public void AnEmptyOptionUsesTheAppDefault() + { + var settingsManager = CreateSettingsManager(); + settingsManager.ConfigurationData.ModelComparison.PreselectOptions = true; + settingsManager.ConfigurationData.ModelComparison.PreselectedProvider = DAILY_PROVIDER_ID; + settingsManager.ConfigurationData.ModelComparison.PreselectedJudgeProvider = string.Empty; + + Assert.That(settingsManager.PreselectedComparisonJudge.Id, Is.EqualTo(APP_DEFAULT_PROVIDER_ID), "\"Use app default\" means the provider of the whole app, not the first model of the comparison."); + } + + [Test] + public void TheOptionsPreselectTheirModels() + { + var settingsManager = CreateSettingsManager(); + settingsManager.ConfigurationData.ModelComparison.PreselectOptions = true; + settingsManager.ConfigurationData.ModelComparison.PreselectedProvider = DAILY_PROVIDER_ID; + settingsManager.ConfigurationData.ModelComparison.PreselectedJudgeProvider = JUDGE_PROVIDER_ID; + + using (Assert.EnterMultipleScope()) + { + Assert.That(settingsManager.PreselectedComparisonModel.Id, Is.EqualTo(DAILY_PROVIDER_ID)); + Assert.That(settingsManager.PreselectedComparisonJudge.Id, Is.EqualTo(JUDGE_PROVIDER_ID)); + } + } + + /// + /// Checks that a preselected model which misses the confidence requirements of the assistant preselects none. + /// + /// + /// Under the default scheme, every provider reaches MEDIUM and a self-hosted one HIGH, so a minimum of HIGH + /// separates the two. + /// + [Test] + public void AModelBelowTheMinimumConfidenceIsNotPreselected() + { + var settingsManager = CreateSettingsManager(); + AddProvider(settingsManager, "44444444-4444-4444-4444-444444444444", LLMProviders.SELF_HOSTED); + settingsManager.ConfigurationData.ModelComparison.PreselectOptions = true; + settingsManager.ConfigurationData.ModelComparison.MinimumProviderConfidence = ConfidenceLevel.HIGH; + settingsManager.ConfigurationData.ModelComparison.PreselectedProvider = DAILY_PROVIDER_ID; + settingsManager.ConfigurationData.ModelComparison.PreselectedJudgeProvider = "44444444-4444-4444-4444-444444444444"; + + using (Assert.EnterMultipleScope()) + { + Assert.That(settingsManager.PreselectedComparisonModel, Is.EqualTo(AIStudio.Settings.Provider.NONE), "A model the assistant may not use must not start a comparison."); + Assert.That(settingsManager.PreselectedComparisonJudge.Id, Is.EqualTo("44444444-4444-4444-4444-444444444444"), "A model which meets the requirements stays preselected."); + } + } + + [Test] + public void ADeletedModelIsNotPreselected() + { + var settingsManager = CreateSettingsManager(); + settingsManager.ConfigurationData.ModelComparison.PreselectOptions = true; + settingsManager.ConfigurationData.ModelComparison.PreselectedJudgeProvider = "55555555-5555-5555-5555-555555555555"; + + Assert.That(settingsManager.PreselectedComparisonJudge, Is.EqualTo(AIStudio.Settings.Provider.NONE)); + } + + /// + /// Builds the settings with three providers, one of them preselected for the whole app. + /// + /// + /// The rust service is handed in as null on purpose: resolving a provider never asks it anything. + /// + private static SettingsManager CreateSettingsManager() + { + var settingsManager = new SettingsManager(NullLogger.Instance, null!); + AddProvider(settingsManager, DAILY_PROVIDER_ID, LLMProviders.OPEN_AI); + AddProvider(settingsManager, JUDGE_PROVIDER_ID, LLMProviders.ANTHROPIC); + AddProvider(settingsManager, APP_DEFAULT_PROVIDER_ID, LLMProviders.MISTRAL); + settingsManager.ConfigurationData.App.PreselectedProvider = APP_DEFAULT_PROVIDER_ID; + return settingsManager; + } + + private static void AddProvider(SettingsManager settingsManager, string id, LLMProviders llmProvider) + { + var providers = settingsManager.ConfigurationData.Providers; + providers.Add(new((uint)providers.Count + 1, id, $"Instance {providers.Count + 1}", llmProvider, new("test-model", null))); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonPresentationOrderTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonPresentationOrderTests.cs new file mode 100644 index 000000000..ed99e7ade --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonPresentationOrderTests.cs @@ -0,0 +1,104 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks which answer stands in which column. +/// +/// +/// A wrong assignment does not crash. It shows the answer of one model under the name of the other +/// once the vote is in, and nothing on the screen looks off. +/// +[TestFixture] +public sealed class ModelComparisonPresentationOrderTests +{ + [TestCase(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, "The first model", "The second model")] + [TestCase(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, "The second model", "The first model")] + public void TheColumnsShowTheAnswersOfTheModelsTheOrderSays(ModelComparisonPresentationOrder order, string expectedInColumnA, string expectedInColumnB) + { + var run = CreateRunResult(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(order.InColumnA(run).Label, Is.EqualTo(expectedInColumnA)); + Assert.That(order.InColumnB(run).Label, Is.EqualTo(expectedInColumnB)); + } + } + + [Test] + public void TheTextOfAColumnIsTheTextOfTheModelWhoseNameItCarries() + { + // The name and the text travel together in one answer, so a column can never mix them up: + var run = CreateRunResult(); + + foreach (var order in Enum.GetValues()) + { + Assert.That(order.InColumnA(run).Text, Does.Contain(order.InColumnA(run).Label), $"{order}, column A"); + Assert.That(order.InColumnB(run).Text, Does.Contain(order.InColumnB(run).Label), $"{order}, column B"); + } + } + + [Test] + public void AnswersInTheTwoColumnsAreNeverTheSame() + { + var run = CreateRunResult(); + + foreach (var order in Enum.GetValues()) + Assert.That(order.InColumnA(run).Label, Is.Not.EqualTo(order.InColumnB(run).Label), order.ToString()); + } + + [Test] + public void EveryOrderIsKnown() + { + var run = CreateRunResult(); + + foreach (var order in Enum.GetValues()) + { + Assert.DoesNotThrow(() => order.InColumnA(run), order.ToString()); + Assert.DoesNotThrow(() => order.InColumnB(run), order.ToString()); + } + } + + [TestCase(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, ModelComparisonVote.COLUMN_A, ModelComparisonModelChoice.FIRST)] + [TestCase(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, ModelComparisonVote.COLUMN_B, ModelComparisonModelChoice.SECOND)] + [TestCase(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, ModelComparisonVote.COLUMN_A, ModelComparisonModelChoice.SECOND)] + [TestCase(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, ModelComparisonVote.COLUMN_B, ModelComparisonModelChoice.FIRST)] + [TestCase(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, ModelComparisonVote.TIE, ModelComparisonModelChoice.TIE)] + [TestCase(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, ModelComparisonVote.TIE, ModelComparisonModelChoice.TIE)] + public void AColumnBasedVoteIsTranslatedBackToItsModel(ModelComparisonPresentationOrder order, ModelComparisonVote vote, ModelComparisonModelChoice expected) + { + Assert.That(order.ToModelChoice(vote), Is.EqualTo(expected)); + } + + [Test] + public void TranslatingAVoteAndBackAgainIsConsistentWithTheColumns() + { + // Whichever model a column names, translating that model's vote back must land on the model actually standing there: + var run = CreateRunResult(); + + foreach (var order in Enum.GetValues()) + { + var modelInColumnA = order.InColumnA(run).Label; + var choiceForColumnA = order.ToModelChoice(ModelComparisonVote.COLUMN_A); + var expectedLabel = choiceForColumnA is ModelComparisonModelChoice.FIRST ? "The first model" : "The second model"; + + Assert.That(modelInColumnA, Is.EqualTo(expectedLabel), order.ToString()); + } + } + + private static ModelComparisonRunResult CreateRunResult() => new() + { + First = new ModelComparisonAnswer + { + Label = "The first model", + Text = "The answer of The first model.", + Completed = true, + }, + Second = new ModelComparisonAnswer + { + Label = "The second model", + Text = "The answer of The second model.", + Completed = true, + }, + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonPresetChoiceTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonPresetChoiceTests.cs new file mode 100644 index 000000000..b420e12f1 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonPresetChoiceTests.cs @@ -0,0 +1,55 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Settings.DataModel; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks which preset the Answer Test of the Model Comparison assistant opens with. +/// +[TestFixture] +public sealed class ModelComparisonPresetChoiceTests +{ + private static readonly ModelComparisonPreset FIRST = new() { Id = Guid.Parse("11111111-1111-1111-1111-111111111111"), Name = "Alpha" }; + private static readonly ModelComparisonPreset SECOND = new() { Id = Guid.Parse("22222222-2222-2222-2222-222222222222"), Name = "Beta" }; + + [Test] + public void ThePreselectedPresetComesFirst() + { + var options = new DataModelComparison { PreselectOptions = true, PreselectedPresetId = SECOND.Id.ToString("D") }; + + Assert.That(ModelComparisonPresetChoice.Choose([FIRST, SECOND], options), Is.EqualTo(SECOND.Id)); + } + + [Test] + public void WithoutOptionsThePreselectedPresetDoesNotCount() + { + var options = new DataModelComparison { PreselectOptions = false, PreselectedPresetId = SECOND.Id.ToString("D") }; + + Assert.That(ModelComparisonPresetChoice.Choose([FIRST, SECOND], options), Is.EqualTo(FIRST.Id), "With the options switched off, the stored preset must not count."); + } + + [Test] + public void ADeletedPreselectedPresetFallsBackToTheFirstOne() + { + var options = new DataModelComparison { PreselectOptions = true, PreselectedPresetId = "33333333-3333-3333-3333-333333333333" }; + + Assert.That(ModelComparisonPresetChoice.Choose([FIRST, SECOND], options), Is.EqualTo(FIRST.Id)); + } + + [Test] + public void ThePresetIdCountsWhateverItsCase() + { + var options = new DataModelComparison { PreselectOptions = true, PreselectedPresetId = "BBBBBBBB-2222-2222-2222-222222222222" }; + var preset = new ModelComparisonPreset { Id = Guid.Parse("bbbbbbbb-2222-2222-2222-222222222222"), Name = "Gamma" }; + + Assert.That(ModelComparisonPresetChoice.Choose([FIRST, preset], options), Is.EqualTo(preset.Id)); + } + + [Test] + public void WithoutPresetsANewOneIsNeeded() + { + var options = new DataModelComparison { PreselectOptions = true, PreselectedPresetId = SECOND.Id.ToString("D") }; + + Assert.That(ModelComparisonPresetChoice.Choose([], options), Is.Null); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonProviderAdapterTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonProviderAdapterTests.cs new file mode 100644 index 000000000..a82a4cca4 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonProviderAdapterTests.cs @@ -0,0 +1,58 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; +using AIStudio.Settings; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks that the model comparison asks a provider only when every chat of the app would ask it. +/// +/// +/// The settings are reached through Program.SERVICE_PROVIDER, as in the app, because the check of a chat +/// reads them from there, see ProhibitedProvidersTests. +/// +[TestFixture] +public sealed class ModelComparisonProviderAdapterTests +{ + private SettingsManager settingsManager = null!; + private ServiceProvider serviceProvider = null!; + private IServiceProvider previousServiceProvider = null!; + + [SetUp] + public void CreateSettings() + { + this.settingsManager = new SettingsManager(NullLogger.Instance, null!); + + this.previousServiceProvider = Program.SERVICE_PROVIDER; + this.serviceProvider = new ServiceCollection().AddSingleton(this.settingsManager).BuildServiceProvider(); + Program.SERVICE_PROVIDER = this.serviceProvider; + } + + [TearDown] + public void RestoreApplicationState() + { + Program.SERVICE_PROVIDER = this.previousServiceProvider; + this.serviceProvider.Dispose(); + } + + [Test] + public async Task AProviderWhichTheOrganizationProhibitedIsNotAsked() + { + var provider = new AIStudio.Settings.Provider(1, Guid.NewGuid().ToString(), "Instance 1", LLMProviders.DEEP_SEEK, new("test-model", null)); + var participant = ModelComparisonProviderAdapter.CreateParticipant(provider, this.settingsManager, AIStudio.Tools.Components.MODEL_COMPARISON_ASSISTANT, "Model Comparison"); + + // Prohibited after it was selected, as an organization may do at any time: + this.settingsManager.ConfigurationData.App.ProhibitedProviders.Add(LLMProviders.DEEP_SEEK); + + // Thrown before any request leaves the app; the runner turns it into a model which did not answer: + await Assert.ThrowsAsync(async () => + { + await foreach (var _ in participant.Ask("A question.", CancellationToken.None)) + { + } + }); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonRecordTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonRecordTests.cs new file mode 100644 index 000000000..44c60ccd6 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonRecordTests.cs @@ -0,0 +1,127 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks what the Model Comparison assistant keeps of a batch for the statistics. +/// +[TestFixture] +public sealed class ModelComparisonRecordTests +{ + private static readonly ModelComparisonModelIdentity FIRST = new() { LLMProvider = LLMProviders.OPEN_AI, ModelId = "first-model", Label = "First" }; + private static readonly ModelComparisonModelIdentity SECOND = new() { LLMProvider = LLMProviders.MISTRAL, ModelId = "second-model", Label = "Second" }; + + [Test] + public void TheRunsAreKeptInTheTermsOfTheModels() + { + var batch = new ModelComparisonBatch(2); + + // Column B is the first model here, and the judge prefers it too: + batch.Add(Entry(ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, ModelComparisonVote.COLUMN_B)); + batch.Vote(ModelComparisonVote.COLUMN_B); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, judgePreferred: null)); + batch.SkipThisRun(); + + var record = ModelComparisonRecord.Of(batch, DateTimeOffset.UtcNow, "Summarize it.", FIRST, SECOND, judge: null); + + using (Assert.EnterMultipleScope()) + { + Assert.That(record.Id, Is.EqualTo(batch.Id), "Storing the same batch again replaces its file."); + Assert.That(record.Runs, Has.Count.EqualTo(2)); + Assert.That(record.Runs[0].Vote, Is.EqualTo(ModelComparisonModelChoice.FIRST)); + Assert.That(record.Runs[0].JudgeChoice, Is.EqualTo(ModelComparisonModelChoice.FIRST)); + Assert.That(record.Runs[1].Vote, Is.Null); + Assert.That(record.Runs[1].JudgeChoice, Is.Null); + Assert.That(record.HoldsAnyChoice, Is.True); + } + } + + [Test] + public void TheTimesAreKeptButNotTheAnswers() + { + var batch = new ModelComparisonBatch(1); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, judgePreferred: null)); + + var record = ModelComparisonRecord.Of(batch, DateTimeOffset.UtcNow, "Summarize it.", FIRST, SECOND, judge: null); + + using (Assert.EnterMultipleScope()) + { + Assert.That(record.Runs[0].First.Answered, Is.True); + Assert.That(record.Runs[0].First.FirstTokenTime, Is.EqualTo(TimeSpan.FromSeconds(1))); + Assert.That(record.Runs[0].First.TotalTime, Is.EqualTo(TimeSpan.FromSeconds(4))); + Assert.That(record.Runs[0].Second.Answered, Is.False); + Assert.That(record.Runs[0].Second.TotalTime, Is.Null); + } + } + + [Test] + public void ABatchWithoutAnyChoiceHoldsNothingToCount() + { + var batch = new ModelComparisonBatch(1); + batch.Add(Entry(ModelComparisonPresentationOrder.FIRST_MODEL_FIRST, judgePreferred: null)); + batch.SkipThisRun(); + + var record = ModelComparisonRecord.Of(batch, DateTimeOffset.UtcNow, "Summarize it.", FIRST, SECOND, judge: null); + + Assert.That(record.HoldsAnyChoice, Is.False); + } + + [Test] + public void TheTimeIsKeptInUtc() + { + var batch = new ModelComparisonBatch(1); + var finished = new DateTimeOffset(2026, 11, 13, 10, 0, 0, TimeSpan.FromHours(1)); + + var record = ModelComparisonRecord.Of(batch, finished, "Summarize it.", FIRST, SECOND, judge: null); + + using (Assert.EnterMultipleScope()) + { + Assert.That(record.FinishedAtUtc.Offset, Is.EqualTo(TimeSpan.Zero)); + Assert.That(record.FinishedAtUtc, Is.EqualTo(finished)); + } + } + + [Test] + public void OnlyASelfHostedModelHasAHost() + { + var cloud = new AIStudio.Settings.Provider(1, Guid.NewGuid().ToString(), "Cloud", LLMProviders.OPEN_AI, new("gpt-model", null)); + var selfHosted = new AIStudio.Settings.Provider(2, Guid.NewGuid().ToString(), "Server", LLMProviders.SELF_HOSTED, new("llama", null), IsSelfHosted: true, Hostname: "http://gpu-server:8080"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(ModelComparisonModelIdentity.Of(cloud).Hostname, Is.Empty); + Assert.That(ModelComparisonModelIdentity.Of(selfHosted).Hostname, Is.EqualTo("http://gpu-server:8080")); + Assert.That(ModelComparisonModelIdentity.Of(selfHosted).ModelId, Is.EqualTo("llama")); + } + } + + [Test] + public void TheSameModelIsRecognizedWhateverItsLabel() + { + var renamed = FIRST with { Label = "Renamed" }; + var otherModel = FIRST with { ModelId = "another-model" }; + var otherHost = FIRST with { Hostname = "http://elsewhere" }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(FIRST.IsSameModelAs(renamed), Is.True); + Assert.That(FIRST.IsSameModelAs(otherModel), Is.False); + Assert.That(FIRST.IsSameModelAs(otherHost), Is.False); + Assert.That(FIRST.IsSameModelAs(SECOND), Is.False); + } + } + + private static ModelComparisonBatchEntry Entry(ModelComparisonPresentationOrder order, ModelComparisonVote? judgePreferred) => new() + { + PresentationOrder = order, + Result = new ModelComparisonRunResult + { + First = new ModelComparisonAnswer { Label = "First", Text = "An answer.", Completed = true, FirstTokenTime = TimeSpan.FromSeconds(1), TotalTime = TimeSpan.FromSeconds(4) }, + Second = judgePreferred is null + ? new ModelComparisonAnswer { Label = "Second", Text = string.Empty, Completed = false } + : new ModelComparisonAnswer { Label = "Second", Text = "Another answer.", Completed = true, FirstTokenTime = TimeSpan.FromSeconds(2), TotalTime = TimeSpan.FromSeconds(3) }, + Judge = judgePreferred is { } preferred ? new ModelComparisonJudgeVerdict { Label = "Judge", Completed = true, Preferred = preferred } : null, + }, + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonRequestTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonRequestTests.cs new file mode 100644 index 000000000..d1a93c9ee --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonRequestTests.cs @@ -0,0 +1,163 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks the text both models receive. +/// +/// +/// A difference in this text would be measured as a difference between the models, so what matters +/// is that the shape stays the same, and that a request without a context is not dressed up. +/// +[TestFixture] +public sealed class ModelComparisonRequestTests +{ + [Test] + public void AQuestionAloneIsSentAsItIs() + { + var request = new ModelComparisonRequest(string.Empty, "What is the capital of France?"); + + Assert.That(request.ToPrompt(), Is.EqualTo("What is the capital of France?")); + } + + [Test] + public void TheMaterialAndAQuestionAreSeparatedByHeadings() + { + var request = new ModelComparisonRequest("The document.", "Summarize it."); + + Assert.That(request.ToPrompt(), Is.EqualTo("# Material\nThe document.\n\n# Question or task\nSummarize it.")); + } + + /// + /// Checks that the request can stand below a heading of another prompt as one part of it. + /// + [Test] + public void TheHeadingsCanStandOneLevelDeeper() + { + var request = new ModelComparisonRequest("The document.", "Summarize it."); + + Assert.That(request.ToPrompt(headingLevel: 2), Is.EqualTo("## Material\nThe document.\n\n## Question or task\nSummarize it.")); + } + + [Test] + public void AHeadingLevelBelowOneIsRefused() + { + var request = new ModelComparisonRequest("The document.", "Summarize it."); + + Assert.Throws(() => request.ToPrompt(headingLevel: 0)); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase("\r\n\t\n")] + public void AContextOfWhiteSpaceCountsAsNone(string context) + { + var request = new ModelComparisonRequest(context, "Summarize it."); + + using (Assert.EnterMultipleScope()) + { + Assert.That(request.Context, Is.Empty); + Assert.That(request.ToPrompt(), Is.EqualTo("Summarize it.")); + } + } + + [Test] + public void WhiteSpaceAroundTheTextsIsDropped() + { + var request = new ModelComparisonRequest("\n\nThe document.\n", " Summarize it. "); + + using (Assert.EnterMultipleScope()) + { + Assert.That(request.Context, Is.EqualTo("The document.")); + Assert.That(request.Question, Is.EqualTo("Summarize it.")); + } + } + + [Test] + public void AShortQuestionIsItsOwnPreview() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it."); + + Assert.That(request.GetQuestionPreview(), Is.EqualTo("Summarize it.")); + } + + [Test] + public void ALongQuestionIsCutAndMarked() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize this document in a few short sentences."); + + Assert.That(request.GetQuestionPreview(18), Is.EqualTo("Summarize this doc…")); + } + + [Test] + public void ACutDoesNotLeaveHalfOfACharacter() + { + // The emoji is two chars long. A cut after the first of them would leave a lone surrogate: + var request = new ModelComparisonRequest(string.Empty, "aaaa😀bbb"); + + Assert.That(request.GetQuestionPreview(5), Is.EqualTo("aaaa…")); + } + + [Test] + public void ThePreviewOfAnEmptyLengthIsRefused() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it."); + + Assert.Throws(() => request.GetQuestionPreview(0)); + } + + [Test] + public void TheSameInputAlwaysBuildsTheSamePrompt() + { + var first = new ModelComparisonRequest("The document.", "Summarize it."); + var second = new ModelComparisonRequest("The document.", "Summarize it."); + + Assert.That(first.ToPrompt(), Is.EqualTo(second.ToPrompt())); + } + + [Test] + public void WithoutALanguageNameThePromptNamesNone() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it."); + + Assert.That(request.ToPrompt(), Does.Not.Contain("Answer in")); + } + + [Test] + public void ALanguageNameIsAppendedAsAnInstruction() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it.", "German"); + + Assert.That(request.ToPrompt(), Is.EqualTo("Summarize it.\n\nAnswer in German.")); + } + + [Test] + public void WithoutACacheBusterThePromptIsUnchanged() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it."); + + Assert.That(request.ToPrompt(), Is.EqualTo(request.ToPrompt(string.Empty))); + } + + [Test] + public void ACacheBusterIsAppendedAsAPlainlyLabeledLine() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it."); + + var prompt = request.ToPrompt("a1b2c3"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompt, Does.StartWith("Summarize it.")); + Assert.That(prompt, Does.Contain("a1b2c3")); + } + } + + [Test] + public void TwoDifferentCacheBustersBuildDifferentPrompts() + { + var request = new ModelComparisonRequest(string.Empty, "Summarize it."); + + Assert.That(request.ToPrompt("one"), Is.Not.EqualTo(request.ToPrompt("two"))); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonRunnerTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonRunnerTests.cs new file mode 100644 index 000000000..b1208dde6 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonRunnerTests.cs @@ -0,0 +1,513 @@ +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +using AIStudio.Assistants.ModelComparison; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how a comparison run treats two models: that they are asked the same, at the same time, +/// and that whatever one of them does cannot spoil the result of the other. +/// +/// +/// The models are made up here. What is under test is the run, not a provider. +/// +[TestFixture] +public sealed class ModelComparisonRunnerTests +{ + private static readonly ModelComparisonRequest REQUEST = new("The document.", "Summarize it."); + + private static readonly TimeSpan PATIENCE = TimeSpan.FromSeconds(5); + + [Test] + public async Task BothModelsReceiveTheSamePrompt() + { + // Not REQUEST.ToPrompt() itself: the run adds a cache-busting request ID neither call here supplies: + var prompts = new ConcurrentBag(); + + IAsyncEnumerable Ask(string prompt, CancellationToken _) + { + prompts.Add(prompt); + return Chunks("An answer."); + } + + await CreateRunner().RunAsync(CreateParticipant("1", Ask), CreateParticipant("2", Ask), REQUEST, CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(prompts, Has.Count.EqualTo(2)); + Assert.That(prompts.Distinct().Count(), Is.EqualTo(1), "Both models must receive the exact same prompt as each other."); + } + } + + [Test] + public async Task RepeatedRunsGetDifferentPromptsSoACachingGatewayCannotAnswerFromACache() + { + var prompts = new ConcurrentBag(); + + IAsyncEnumerable Ask(string prompt, CancellationToken _) + { + prompts.Add(prompt); + return Chunks("An answer."); + } + + var runner = CreateRunner(); + await runner.RunAsync(CreateParticipant("1", Ask), CreateParticipant("2", Ask), REQUEST, CancellationToken.None); + await runner.RunAsync(CreateParticipant("1", Ask), CreateParticipant("2", Ask), REQUEST, CancellationToken.None); + + // Both models of the same run still share one prompt, the same as in BothModelsReceiveTheSamePrompt; + // it is the two runs that must differ from each other, so a caching gateway cannot answer a + // repeat run from what it already has cached for the first one: + Assert.That(prompts.Distinct().Count(), Is.EqualTo(2), "Each run must see a prompt of its own, distinct from the other run's."); + } + + [Test] + public async Task TheModelsAreAskedAtTheSameTime() + { + var started = 0; + var bothStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // + // Neither model answers before the other one has been asked. Asked one after the other, the + // first would wait in vain, run out of patience and fail, and so would the assertion below. + // + async IAsyncEnumerable Ask(string _, [EnumeratorCancellation] CancellationToken token) + { + if (Interlocked.Increment(ref started) == 2) + bothStarted.SetResult(); + + await bothStarted.Task.WaitAsync(PATIENCE, token); + yield return "An answer."; + } + + var result = await CreateRunner().RunAsync(CreateParticipant("1", Ask), CreateParticipant("2", Ask), REQUEST, CancellationToken.None); + + Assert.That(result.BothCompleted, Is.True); + } + + [Test] + public async Task EachAnswerStaysWithTheModelWhichGaveIt() + { + var secondIsDone = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // The first model finishes last, so a result assembled in the order of arrival would swap them: + async IAsyncEnumerable AskFirst(string _, [EnumeratorCancellation] CancellationToken token) + { + await secondIsDone.Task.WaitAsync(PATIENCE, token); + yield return "from the first"; + } + + async IAsyncEnumerable AskSecond(string _, [EnumeratorCancellation] CancellationToken __) + { + yield return "from the second"; + secondIsDone.TrySetResult(); + await Task.CompletedTask; + } + + var result = await CreateRunner().RunAsync(CreateParticipant("1", AskFirst), CreateParticipant("2", AskSecond), REQUEST, CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.Text, Is.EqualTo("from the first")); + Assert.That(result.First.Label, Is.EqualTo("Model 1")); + Assert.That(result.Second.Text, Is.EqualTo("from the second")); + Assert.That(result.Second.Label, Is.EqualTo("Model 2")); + } + } + + [Test] + public async Task AnAnswerIsTheChunksJoinedWithoutTheThinking() + { + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("Let me think.", "", "Hello", " world ")), + CreateParticipant("2", (_, _) => Chunks("Hi")), + REQUEST, + CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.Text, Is.EqualTo("Hello world")); + Assert.That(result.First.Completed, Is.True); + } + } + + [Test] + public async Task TheTimesAreMeasuredOnTheClockOfTheRun() + { + var clock = new ManualTimeProvider(); + + async IAsyncEnumerable AskFirst(string _, [EnumeratorCancellation] CancellationToken __) + { + clock.Advance(TimeSpan.FromMilliseconds(200)); + yield return "Hi"; + + clock.Advance(TimeSpan.FromMilliseconds(300)); + yield return " there"; + await Task.CompletedTask; + } + + // The second model does not touch the clock, so the times of the first one are the only ones which move: + var result = await CreateRunner(clock).RunAsync( + CreateParticipant("1", AskFirst), + CreateParticipant("2", (_, _) => Failing()), + REQUEST, + CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.FirstTokenTime, Is.EqualTo(TimeSpan.FromMilliseconds(200))); + Assert.That(result.First.TotalTime, Is.EqualTo(TimeSpan.FromMilliseconds(500))); + } + } + + [Test] + public async Task AModelWhichFailsDoesNotTakeTheOtherOneDown() + { + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Failing()), + REQUEST, + CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.Completed, Is.True); + Assert.That(result.First.Text, Is.EqualTo("An answer.")); + Assert.That(result.Second.Completed, Is.False); + Assert.That(result.Second.TotalTime, Is.Null, "A failed request has no total time."); + Assert.That(result.BothCompleted, Is.False); + } + } + + [Test] + public async Task AnAnswerWhichSaysNothingCountsAsAFailure() + { + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks()), + CreateParticipant("2", (_, _) => Chunks(" ", "\n")), + REQUEST, + CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.Completed, Is.False); + Assert.That(result.Second.Completed, Is.False); + Assert.That(result.BothCompleted, Is.False); + } + } + + [Test] + public async Task AnAnswerWhichEndsInsideTheThinkingCountsAsAFailure() + { + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("Still thinking, and out of room")), + CreateParticipant("2", (_, _) => Chunks("An answer.")), + REQUEST, + CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.Completed, Is.False); + Assert.That(result.First.Text, Is.Empty); + Assert.That(result.Second.Completed, Is.True); + } + } + + [Test] + public async Task ACanceledRunEndsBothModelsWithoutACompletedAnswer() + { + using var cancellation = new CancellationTokenSource(); + + async IAsyncEnumerable Ask(string _, [EnumeratorCancellation] CancellationToken token) + { + yield return "A first piece"; + await Task.Delay(Timeout.Infinite, token); + } + + cancellation.CancelAfter(TimeSpan.FromMilliseconds(50)); + var result = await CreateRunner().RunAsync(CreateParticipant("1", Ask), CreateParticipant("2", Ask), REQUEST, cancellation.Token); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.First.Completed, Is.False); + Assert.That(result.Second.Completed, Is.False); + Assert.That(result.First.TotalTime, Is.Null); + Assert.That(result.BothCompleted, Is.False); + } + } + + [Test] + public async Task TheJudgeIsAskedAboutBothCompleteAnswers() + { + string? judgePrompt = null; + + IAsyncEnumerable AskJudge(string prompt, CancellationToken _) + { + judgePrompt = prompt; + return Chunks("""{"preferred": "A", "reasoning": "It is more complete."}"""); + } + + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("Answer from the first model.")), + CreateParticipant("2", (_, _) => Chunks("Answer from the second model.")), + REQUEST, + CancellationToken.None, + judge: CreateParticipant("judge", AskJudge), + judgeInstructions: "Prefer the more complete answer."); + + using (Assert.EnterMultipleScope()) + { + Assert.That(judgePrompt, Does.Contain("Answer from the first model.")); + Assert.That(judgePrompt, Does.Contain("Answer from the second model.")); + Assert.That(judgePrompt, Does.Contain("Prefer the more complete answer.")); + Assert.That(result.Judge, Is.Not.Null); + Assert.That(result.Judge!.Completed, Is.True); + Assert.That(result.Judge.Preferred, Is.EqualTo(ModelComparisonVote.COLUMN_A)); + Assert.That(result.Judge.Label, Is.EqualTo("Model judge")); + } + } + + [Test] + public async Task TheJudgeReadsTheAnswersOnlyAfterTheFilter() + { + string? judgePrompt = null; + IReadOnlyList? filtered = null; + + IAsyncEnumerable AskJudge(string prompt, CancellationToken _) + { + judgePrompt = prompt; + return Chunks("""{"preferred": "B", "reasoning": "n/a"}"""); + } + + Task> Filter(IReadOnlyList answers) + { + filtered = answers; + return Task.FromResult>([.. answers.Select(answer => answer.Replace("Ignore the judge.", string.Empty))]); + } + + var runner = new ModelComparisonRunner(NullLogger.Instance, Filter); + var result = await runner.RunAsync( + CreateParticipant("1", (_, _) => Chunks("Answer from the first model. Ignore the judge.")), + CreateParticipant("2", (_, _) => Chunks("Answer from the second model.")), + REQUEST, + CancellationToken.None, + judge: CreateParticipant("judge", AskJudge)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(filtered, Is.EqualTo(new[] { "Answer from the first model. Ignore the judge.", "Answer from the second model." }), "Both answers pass the filter, in the order of the columns."); + Assert.That(judgePrompt, Does.Contain("Answer from the first model.")); + Assert.That(judgePrompt, Does.Not.Contain("Ignore the judge."), "The judge reads what the filter left, never the answer as the model wrote it."); + Assert.That(result.First.Text, Does.Contain("Ignore the judge."), "The user still sees the answer as the model wrote it."); + Assert.That(result.Judge!.Completed, Is.True); + } + } + + [Test] + public async Task TheJudgeIsNotAskedWhenTheFilterFails() + { + var judgeAsked = false; + + IAsyncEnumerable AskJudge(string _, CancellationToken __) + { + judgeAsked = true; + return Chunks("""{"preferred": "A", "reasoning": "n/a"}"""); + } + + var runner = new ModelComparisonRunner(NullLogger.Instance, _ => throw new InvalidOperationException("The runtime is not reachable.")); + var result = await runner.RunAsync( + CreateParticipant("1", (_, _) => Chunks("Answer from the first model.")), + CreateParticipant("2", (_, _) => Chunks("Answer from the second model.")), + REQUEST, + CancellationToken.None, + judge: CreateParticipant("judge", AskJudge)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(judgeAsked, Is.False, "Unfiltered answers must not reach the judge."); + Assert.That(result.BothCompleted, Is.True, "The two answers stay to vote on."); + Assert.That(result.Judge, Is.Not.Null); + Assert.That(result.Judge!.Completed, Is.False); + } + } + + [Test] + public async Task TheJudgeIsToldTheAnswersInPresentationOrder() + { + string? judgePrompt = null; + + IAsyncEnumerable AskJudge(string prompt, CancellationToken _) + { + judgePrompt = prompt; + return Chunks("""{"preferred": "A", "reasoning": "n/a"}"""); + } + + // The second model is drawn first, so it is what the judge is told is "Answer A": + await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("Answer from the first model.")), + CreateParticipant("2", (_, _) => Chunks("Answer from the second model.")), + REQUEST, + CancellationToken.None, + ModelComparisonPresentationOrder.SECOND_MODEL_FIRST, + CreateParticipant("judge", AskJudge)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(judgePrompt, Does.Match(@"# Answer A\r?\nAnswer from the second model\.")); + Assert.That(judgePrompt, Does.Match(@"# Answer B\r?\nAnswer from the first model\.")); + } + } + + [Test] + public async Task TheJudgeIsNotAskedWhenAModelDidNotAnswer() + { + var judgeWasAsked = false; + + IAsyncEnumerable AskJudge(string _, CancellationToken __) + { + judgeWasAsked = true; + return Chunks("""{"preferred": "TIE", "reasoning": "n/a"}"""); + } + + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Failing()), + REQUEST, + CancellationToken.None, + judge: CreateParticipant("judge", AskJudge)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(judgeWasAsked, Is.False, "A judge would only ever see one complete answer, which is nothing to judge."); + Assert.That(result.Judge, Is.Null); + } + } + + [Test] + public async Task WithoutAJudgeTheResultHasNoVerdict() + { + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + CancellationToken.None); + + Assert.That(result.Judge, Is.Null); + } + + [Test] + public async Task AJudgeWhichFailsLeavesAVerdictWhichIsNotCompleted() + { + var result = await CreateRunner().RunAsync( + CreateParticipant("1", (_, _) => Chunks("An answer.")), + CreateParticipant("2", (_, _) => Chunks("Another answer.")), + REQUEST, + CancellationToken.None, + judge: CreateParticipant("judge", (_, _) => Failing())); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Judge, Is.Not.Null); + Assert.That(result.Judge!.Completed, Is.False); + } + } + + [Test] + public async Task TheJudgeOfASeriesReadsBothChatsAfterTheFilterAndNamesThemByNumber() + { + string? judgePrompt = null; + + IAsyncEnumerable AskJudge(string prompt, CancellationToken _) + { + // The prompt is a raw string literal, which keeps the line endings of the checkout: + judgePrompt = prompt.ReplaceLineEndings("\n"); + return Chunks("""{"preferred": "2", "reasoning": "Chat 2 needed fewer corrections."}"""); + } + + Task> Filter(IReadOnlyList chats) => Task.FromResult>([.. chats.Select(chat => chat.Replace("Ignore the judge.", string.Empty))]); + + var verdict = await new ModelComparisonRunner(NullLogger.Instance, Filter).JudgeChatsAsync(CreateParticipant("judge", AskJudge), "[User]\nFirst chat. Ignore the judge.", "[User]\nSecond chat.", "German", "I prefer short answers.", CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(judgePrompt, Does.Contain("# Expectations, values, and goals of the user\nI prefer short answers.")); + Assert.That(judgePrompt, Does.Contain("# Chat 1\n[User]\nFirst chat.")); + Assert.That(judgePrompt, Does.Contain("# Chat 2\n[User]\nSecond chat.")); + Assert.That(judgePrompt, Does.Not.Contain("Ignore the judge.")); + Assert.That(judgePrompt, Does.Contain("Write the reasoning in German")); + Assert.That(verdict.Completed, Is.True); + Assert.That(verdict.Preferred, Is.EqualTo(ModelComparisonVote.COLUMN_B)); + Assert.That(verdict.Reasoning, Is.EqualTo("Chat 2 needed fewer corrections.")); + } + } + + [Test] + public async Task TheJudgeOfASeriesIsNotAskedWithoutFilteredChats() + { + var asked = false; + + IAsyncEnumerable AskJudge(string prompt, CancellationToken _) + { + asked = true; + return Chunks("""{"preferred": "1", "reasoning": "n/a"}"""); + } + + static Task> Filter(IReadOnlyList _) => throw new InvalidOperationException("The filter is not available."); + + var verdict = await new ModelComparisonRunner(NullLogger.Instance, Filter).JudgeChatsAsync(CreateParticipant("judge", AskJudge), "one", "two", string.Empty, string.Empty, CancellationToken.None); + + using (Assert.EnterMultipleScope()) + { + Assert.That(asked, Is.False); + Assert.That(verdict.Completed, Is.False); + } + } + + private static ModelComparisonRunner CreateRunner(TimeProvider? clock = null) => new(NullLogger.Instance, Unfiltered, clock); + + // The filter of the app needs the runtime; what is under test here is the run: + private static Task> Unfiltered(IReadOnlyList answers) => Task.FromResult(answers); + + private static ModelComparisonParticipant CreateParticipant(string id, Func> ask) => new() + { + Label = $"Model {id}", + Ask = ask, + }; + + private static async IAsyncEnumerable Chunks(params string[] chunks) + { + foreach (var chunk in chunks) + { + await Task.Yield(); + yield return chunk; + } + } + + private static async IAsyncEnumerable Failing() + { + await Task.Yield(); + throw new InvalidOperationException("The model is not reachable."); + + #pragma warning disable CS0162 // Unreachable code: the yield is what makes this an iterator + // ReSharper disable once HeuristicUnreachableCode + yield break; + #pragma warning restore CS0162 + } + + /// + /// A clock which only moves when a test says so. + /// + private sealed class ManualTimeProvider : TimeProvider + { + private long ticks; + + // One timestamp is one tick of a TimeSpan, so the elapsed time of the runner is exactly what Advance added: + public override long TimestampFrequency => TimeSpan.TicksPerSecond; + + public void Advance(TimeSpan by) => Interlocked.Add(ref this.ticks, by.Ticks); + + public override long GetTimestamp() => Interlocked.Read(ref this.ticks); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesCopyTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesCopyTests.cs new file mode 100644 index 000000000..9ba59d975 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesCopyTests.cs @@ -0,0 +1,239 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks what a new comparison series takes over from an existing one, and what it leaves out. +/// +[TestFixture] +public sealed class ModelComparisonSeriesCopyTests +{ + private const string FIRST_PROVIDER_ID = "11111111-1111-1111-1111-111111111111"; + private const string SECOND_PROVIDER_ID = "22222222-2222-2222-2222-222222222222"; + private const string JUDGE_PROVIDER_ID = "33333333-3333-3333-3333-333333333333"; + private const string PROFILE_ID = "44444444-4444-4444-4444-444444444444"; + private const string DATA_SOURCE_ID = "55555555-5555-5555-5555-555555555555"; + private const string OTHER_DATA_SOURCE_ID = "66666666-6666-6666-6666-666666666666"; + private const string TOOL_ID = "web_search"; + private const string OTHER_TOOL_ID = "read_web_page"; + + [Test] + public void AnAvailableSetupIsTakenOverCompletely() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager); + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID, OTHER_TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.First.Id, Is.EqualTo(FIRST_PROVIDER_ID)); + Assert.That(copy.Second.Id, Is.EqualTo(SECOND_PROVIDER_ID)); + Assert.That(copy.JudgeEnabled, Is.True); + Assert.That(copy.Judge.Id, Is.EqualTo(JUDGE_PROVIDER_ID)); + Assert.That(copy.Profile.Id, Is.EqualTo(PROFILE_ID)); + Assert.That(copy.ToolIds, Is.EquivalentTo(new[] { TOOL_ID, OTHER_TOOL_ID })); + Assert.That(copy.DataSourceOptions.DisableDataSources, Is.False); + Assert.That(copy.DataSourceOptions.PreselectedDataSourceIds, Is.EquivalentTo(new[] { DATA_SOURCE_ID, OTHER_DATA_SOURCE_ID })); + Assert.That(copy.DataSourceOptions.RetrievalMode, Is.EqualTo(DataSourceRetrievalMode.EVERY_MESSAGE), "Both chats of the new series must search the same way as before."); + Assert.That(copy.DataSourceOptions.AutomaticDataSourceSelection, Is.False, "An AI never selects the data sources of a series."); + Assert.That(copy.DataSourceOptions.AutomaticValidation, Is.False, "An AI never validates what the data sources of a series gave."); + Assert.That(copy.MissingParts, Is.Empty); + } + } + + /// + /// Checks that a model which is gone or which is another one now has to be chosen anew. + /// + /// + /// A provider whose model changed would otherwise carry the old comparison over to a model the user never chose for + /// it. + /// + [Test] + public void ADeletedOrChangedModelIsLeftOut() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager); + var providers = settingsManager.ConfigurationData.Providers; + providers.RemoveAll(provider => provider.Id == FIRST_PROVIDER_ID); + var second = providers.Single(provider => provider.Id == SECOND_PROVIDER_ID); + providers[providers.IndexOf(second)] = second with { Model = new("another-model", null) }; + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID, OTHER_TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.First, Is.EqualTo(AIStudio.Settings.Provider.NONE)); + Assert.That(copy.Second, Is.EqualTo(AIStudio.Settings.Provider.NONE)); + Assert.That(copy.MissingParts, Is.EquivalentTo(new[] { ModelComparisonSeriesPart.FIRST_MODEL, ModelComparisonSeriesPart.SECOND_MODEL })); + } + } + + /// + /// Checks that a model which the assistant may no longer use has to be chosen anew. + /// + /// + /// Under the default scheme, every provider reaches MEDIUM and a self-hosted one HIGH, so a minimum of HIGH leaves + /// out the two compared models, but not the judge. + /// + [Test] + public void AModelBelowTheMinimumConfidenceIsLeftOut() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager); + settingsManager.ConfigurationData.ModelComparison.PreselectOptions = true; + settingsManager.ConfigurationData.ModelComparison.MinimumProviderConfidence = ConfidenceLevel.HIGH; + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID, OTHER_TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.Judge.Id, Is.EqualTo(JUDGE_PROVIDER_ID)); + Assert.That(copy.MissingParts, Is.EquivalentTo(new[] { ModelComparisonSeriesPart.FIRST_MODEL, ModelComparisonSeriesPart.SECOND_MODEL })); + } + } + + /// + /// Checks that the judge stays switched on when its model is gone: the user wanted a judge, and chooses another one. + /// + [Test] + public void AMissingJudgeKeepsTheJudgeSwitchedOn() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager); + settingsManager.ConfigurationData.Providers.RemoveAll(provider => provider.Id == JUDGE_PROVIDER_ID); + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID, OTHER_TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.JudgeEnabled, Is.True); + Assert.That(copy.Judge, Is.EqualTo(AIStudio.Settings.Provider.NONE)); + Assert.That(copy.MissingParts, Is.EquivalentTo(new[] { ModelComparisonSeriesPart.JUDGE })); + } + } + + [Test] + public void ASeriesWithoutJudgeStartsWithoutJudge() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager) with { JudgeProviderId = string.Empty, Judge = null }; + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID, OTHER_TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.JudgeEnabled, Is.False); + Assert.That(copy.Judge, Is.EqualTo(AIStudio.Settings.Provider.NONE)); + Assert.That(copy.MissingParts, Is.Empty, "A series without a judge misses nothing."); + } + } + + [Test] + public void AMissingProfileToolAndDataSourceAreLeftOut() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager); + settingsManager.ConfigurationData.Profiles.Clear(); + settingsManager.ConfigurationData.DataSources.RemoveAll(dataSource => dataSource.Id == OTHER_DATA_SOURCE_ID); + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.Profile, Is.EqualTo(Profile.NO_PROFILE)); + Assert.That(copy.ToolIds, Is.EquivalentTo(new[] { TOOL_ID }), "The tools which are still available stay selected."); + Assert.That(copy.DataSourceOptions.PreselectedDataSourceIds, Is.EquivalentTo(new[] { DATA_SOURCE_ID }), "The data sources which still exist stay selected."); + Assert.That(copy.MissingParts, Is.EquivalentTo(new[] { ModelComparisonSeriesPart.PROFILE, ModelComparisonSeriesPart.TOOLS, ModelComparisonSeriesPart.DATA_SOURCES })); + } + } + + [Test] + public void ASeriesWithoutProfileToolsAndDataSourcesMissesNothing() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager) with { ProfileId = Profile.NO_PROFILE.Id, ToolIds = [], DataSourceIds = [], RetrievalMode = null }; + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.Profile, Is.EqualTo(Profile.NO_PROFILE)); + Assert.That(copy.DataSourceOptions.DisableDataSources, Is.True); + Assert.That(copy.DataSourceOptions.RetrievalMode, Is.EqualTo(DataSourceRetrievalMode.SEMANTIC_SEARCH), "Without data sources, the new series offers Semantic Search first, as every new series does."); + Assert.That(copy.MissingParts, Is.Empty); + } + } + + /// + /// Checks that the data sources are left out while their preview is switched off, since the dialog cannot show them + /// then. + /// + [Test] + public void DataSourcesAreLeftOutWhileTheirPreviewIsSwitchedOff() + { + var settingsManager = CreateSettingsManager(); + var series = CreateSeries(settingsManager); + settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Clear(); + + var copy = ModelComparisonSeriesCopy.Of(settingsManager, series, new HashSet { TOOL_ID, OTHER_TOOL_ID }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(copy.DataSourceOptions.DisableDataSources, Is.True); + Assert.That(copy.DataSourceOptions.PreselectedDataSourceIds, Is.Empty); + Assert.That(copy.MissingParts, Is.EquivalentTo(new[] { ModelComparisonSeriesPart.DATA_SOURCES })); + } + } + + /// + /// Builds a series whose whole setup exists in the settings. + /// + private static ModelComparisonSeries CreateSeries(SettingsManager settingsManager) => new() + { + Id = Guid.NewGuid(), + Name = "Weekly reports", + CreatedAtUtc = DateTimeOffset.UtcNow, + FirstProviderId = FIRST_PROVIDER_ID, + SecondProviderId = SECOND_PROVIDER_ID, + First = ModelComparisonModelIdentity.Of(settingsManager.GetProviderById(FIRST_PROVIDER_ID)), + Second = ModelComparisonModelIdentity.Of(settingsManager.GetProviderById(SECOND_PROVIDER_ID)), + JudgeProviderId = JUDGE_PROVIDER_ID, + Judge = ModelComparisonModelIdentity.Of(settingsManager.GetProviderById(JUDGE_PROVIDER_ID)), + ProfileId = PROFILE_ID, + ToolIds = [TOOL_ID, OTHER_TOOL_ID], + DataSourceIds = [DATA_SOURCE_ID, OTHER_DATA_SOURCE_ID], + RetrievalMode = DataSourceRetrievalMode.EVERY_MESSAGE, + }; + + /// + /// Builds the settings with three providers, a profile, and two data sources, with the preview of data sources + /// switched on. + /// + /// + /// The rust service is handed in as null on purpose: resolving a provider never asks it anything. + /// + private static SettingsManager CreateSettingsManager() + { + var settingsManager = new SettingsManager(NullLogger.Instance, null!); + AddProvider(settingsManager, FIRST_PROVIDER_ID, LLMProviders.OPEN_AI); + AddProvider(settingsManager, SECOND_PROVIDER_ID, LLMProviders.MISTRAL); + AddProvider(settingsManager, JUDGE_PROVIDER_ID, LLMProviders.SELF_HOSTED); + settingsManager.ConfigurationData.Profiles.Add(new(1, PROFILE_ID, "Reports", "I write reports.", string.Empty)); + settingsManager.ConfigurationData.DataSources.Add(new DataSourceLocalDirectory { Id = DATA_SOURCE_ID, Name = "Reports" }); + settingsManager.ConfigurationData.DataSources.Add(new DataSourceLocalDirectory { Id = OTHER_DATA_SOURCE_ID, Name = "Minutes" }); + settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Add(PreviewFeatures.PRE_RAG_2024); + return settingsManager; + } + + private static void AddProvider(SettingsManager settingsManager, string id, LLMProviders llmProvider) + { + var providers = settingsManager.ConfigurationData.Providers; + providers.Add(new((uint)providers.Count + 1, id, $"Instance {providers.Count + 1}", llmProvider, new("test-model", null))); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesStatisticsTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesStatisticsTests.cs new file mode 100644 index 000000000..7da1d4baa --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesStatisticsTests.cs @@ -0,0 +1,138 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks what the result of a comparison series says about its two models. +/// +[TestFixture] +public sealed class ModelComparisonSeriesStatisticsTests +{ + private static readonly DateTimeOffset START = new(2026, 11, 13, 9, 0, 0, TimeSpan.Zero); + + private static readonly ModelComparisonModelIdentity A = new() { LLMProvider = LLMProviders.OPEN_AI, ModelId = "model-a", Label = "Model A" }; + private static readonly ModelComparisonModelIdentity B = new() { LLMProvider = LLMProviders.MISTRAL, ModelId = "model-b", Label = "Model B" }; + private static readonly ModelComparisonModelIdentity JUDGE = new() { LLMProvider = LLMProviders.ANTHROPIC, ModelId = "judge", Label = "Judge" }; + + [Test] + public void TheBlindTasksDecideAndTheOthersStandApart() + { + var statistics = ModelComparisonSeriesStatistics.Of(Series( + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_A), + DecidedTask(firstModelIsChatOne: false, ModelComparisonVote.COLUMN_B), + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.TIE), + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_B, isBlind: false), + ModelComparisonSeriesTask.Begin(START, firstModelIsChatOne: true))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.Pairing.First, Is.EqualTo(A)); + Assert.That(statistics.Pairing.Second, Is.EqualTo(B)); + Assert.That(statistics.Pairing.ComparisonCount, Is.EqualTo(3)); + Assert.That(statistics.Pairing.Votes.FirstModelWins, Is.EqualTo(2)); + Assert.That(statistics.Pairing.Votes.SecondModelWins, Is.Zero); + Assert.That(statistics.Pairing.Votes.Ties, Is.EqualTo(1)); + Assert.That(statistics.NotBlindVotes.TotalRunCount, Is.EqualTo(1)); + Assert.That(statistics.NotBlindVotes.SecondModelWins, Is.EqualTo(1)); + } + } + + [Test] + public void TheWinsOfTheFirstAndTheSecondChatShowWhetherTheOrderMattered() + { + var statistics = ModelComparisonSeriesStatistics.Of(Series( + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_B), + DecidedTask(firstModelIsChatOne: false, ModelComparisonVote.COLUMN_B), + DecidedTask(firstModelIsChatOne: false, ModelComparisonVote.COLUMN_A), + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.TIE))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.WinsOfChatOne, Is.EqualTo(1)); + Assert.That(statistics.WinsOfChatTwo, Is.EqualTo(2)); + } + } + + [Test] + public void TheJudgeAgreesWhenItChoseTheSameModel() + { + var agreeing = DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_A); + agreeing.TakeVerdict(new ModelComparisonJudgeVerdict { Label = "Judge", Completed = true, Preferred = ModelComparisonVote.COLUMN_A }); + + var disagreeing = DecidedTask(firstModelIsChatOne: false, ModelComparisonVote.COLUMN_A); + disagreeing.TakeVerdict(new ModelComparisonJudgeVerdict { Label = "Judge", Completed = true, Preferred = ModelComparisonVote.TIE }); + + var withoutVerdict = DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_B); + + var statistics = ModelComparisonSeriesStatistics.Of(Series(agreeing, disagreeing, withoutVerdict)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.JudgeAgreement, Is.Not.Null); + Assert.That(statistics.JudgeAgreement!.ComparedRuns, Is.EqualTo(2)); + Assert.That(statistics.JudgeAgreement.Agreements, Is.EqualTo(1)); + Assert.That(statistics.Pairing.JudgeVerdicts.FirstModelWins, Is.EqualTo(1)); + Assert.That(statistics.Pairing.JudgeVerdicts.Ties, Is.EqualTo(1)); + } + } + + [Test] + public void TheEffortCountsMessagesAndTheMedianTimeOfEachModel() + { + var statistics = ModelComparisonSeriesStatistics.Of(Series( + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_A, chatOneMinutes: 10, chatOneMessages: 3, chatTwoMinutes: 4, chatTwoMessages: 1), + DecidedTask(firstModelIsChatOne: false, ModelComparisonVote.COLUMN_A, chatOneMinutes: 6, chatOneMessages: 2, chatTwoMinutes: 2, chatTwoMessages: 1), + DecidedTask(firstModelIsChatOne: true, ModelComparisonVote.COLUMN_A, chatOneMinutes: 600, chatOneMessages: 1, chatTwoMinutes: 5, chatTwoMessages: 4))); + + using (Assert.EnterMultipleScope()) + { + // The first model answered in the first chat of the first and the third task, and in the second chat of the second: + Assert.That(statistics.FirstEffort.Model, Is.EqualTo(A)); + Assert.That(statistics.FirstEffort.TaskCount, Is.EqualTo(3)); + Assert.That(statistics.FirstEffort.AverageUserMessages, Is.EqualTo(2.0 / 3 + 1).Within(0.001)); + Assert.That(statistics.FirstEffort.MedianDuration, Is.EqualTo(TimeSpan.FromMinutes(10))); + + Assert.That(statistics.SecondEffort.Model, Is.EqualTo(B)); + Assert.That(statistics.SecondEffort.AverageUserMessages, Is.EqualTo(7.0 / 3).Within(0.001)); + Assert.That(statistics.SecondEffort.MedianDuration, Is.EqualTo(TimeSpan.FromMinutes(5))); + } + } + + [Test] + public void ASeriesWithoutJudgeAndWithoutTasksHasNothingToShow() + { + var series = Series() with { Judge = null }; + var statistics = ModelComparisonSeriesStatistics.Of(series); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.Pairing.ComparisonCount, Is.Zero); + Assert.That(statistics.JudgeAgreement, Is.Null); + Assert.That(statistics.FirstEffort.TaskCount, Is.Zero); + Assert.That(statistics.FirstEffort.AverageUserMessages, Is.Null); + Assert.That(statistics.FirstEffort.MedianDuration, Is.Null); + } + } + + private static ModelComparisonSeries Series(params ModelComparisonSeriesTask[] tasks) => new() + { + Id = Guid.NewGuid(), + Name = "Series", + CreatedAtUtc = START, + First = A, + Second = B, + Judge = JUDGE, + Tasks = [.. tasks], + }; + + private static ModelComparisonSeriesTask DecidedTask(bool firstModelIsChatOne, ModelComparisonVote vote, bool isBlind = true, double chatOneMinutes = 1, int chatOneMessages = 1, double chatTwoMinutes = 1, int chatTwoMessages = 1) + { + var task = ModelComparisonSeriesTask.Begin(START, firstModelIsChatOne); + var chatOneDone = START.AddMinutes(chatOneMinutes); + task.FinishFirstChat(START, chatOneDone, chatOneMessages, "question"); + task.FinishSecondChat(chatOneDone, chatOneDone.AddMinutes(chatTwoMinutes), chatTwoMessages); + task.Decide(vote, chatOneDone.AddMinutes(chatTwoMinutes + 1), isBlind); + return task; + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesTaskTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesTaskTests.cs new file mode 100644 index 000000000..7ff41f49c --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesTaskTests.cs @@ -0,0 +1,160 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how a task of a comparison series goes from the first chat over the second chat to the decision. +/// +[TestFixture] +public sealed class ModelComparisonSeriesTaskTests +{ + private static readonly DateTimeOffset BEGUN = new(2026, 11, 13, 9, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset FIRST_SENT = BEGUN.AddMinutes(2); + private static readonly DateTimeOffset FIRST_DONE = BEGUN.AddMinutes(12); + private static readonly DateTimeOffset SECOND_SENT = BEGUN.AddMinutes(13); + private static readonly DateTimeOffset SECOND_DONE = BEGUN.AddMinutes(20); + private static readonly DateTimeOffset DECIDED = BEGUN.AddMinutes(25); + + [Test] + public void ABegunTaskIsInTheFirstChat() + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne: true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(task.Id, Is.Not.EqualTo(Guid.Empty)); + Assert.That(task.StartedAtUtc, Is.EqualTo(BEGUN)); + Assert.That(task.State, Is.EqualTo(ModelComparisonSeriesTaskState.FIRST_CHAT)); + Assert.That(task.CurrentChat, Is.SameAs(task.ChatOne)); + Assert.That(task.ChatOne.ChatId, Is.Not.EqualTo(Guid.Empty)); + Assert.That(task.ChatTwo.ChatId, Is.Not.EqualTo(Guid.Empty).And.Not.EqualTo(task.ChatOne.ChatId)); + Assert.That(task.ChatOne.StartedAtUtc, Is.Null); + Assert.That(task.IsBlind, Is.True); + } + } + + [Test] + public void BothChatsKeepWhenTheirFirstMessageWasSentAndHowOftenTheUserWrote() + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne: false); + + using (Assert.EnterMultipleScope()) + { + Assert.That(task.FinishFirstChat(FIRST_SENT, FIRST_DONE, 3, "Summarize the report."), Is.True); + Assert.That(task.State, Is.EqualTo(ModelComparisonSeriesTaskState.SECOND_CHAT)); + Assert.That(task.CurrentChat, Is.SameAs(task.ChatTwo)); + Assert.That(task.QuestionPreview, Is.EqualTo("Summarize the report.")); + + Assert.That(task.FinishSecondChat(SECOND_SENT, SECOND_DONE, 2), Is.True); + Assert.That(task.State, Is.EqualTo(ModelComparisonSeriesTaskState.DECIDING)); + Assert.That(task.CurrentChat, Is.Null); + + Assert.That(task.ChatOne.Duration, Is.EqualTo(TimeSpan.FromMinutes(10))); + Assert.That(task.ChatOne.UserMessageCount, Is.EqualTo(3)); + Assert.That(task.ChatTwo.Duration, Is.EqualTo(TimeSpan.FromMinutes(7))); + Assert.That(task.ChatTwo.UserMessageCount, Is.EqualTo(2)); + } + } + + [TestCase(true, ModelComparisonVote.COLUMN_A, ModelComparisonModelChoice.FIRST)] + [TestCase(true, ModelComparisonVote.COLUMN_B, ModelComparisonModelChoice.SECOND)] + [TestCase(false, ModelComparisonVote.COLUMN_A, ModelComparisonModelChoice.SECOND)] + [TestCase(false, ModelComparisonVote.COLUMN_B, ModelComparisonModelChoice.FIRST)] + [TestCase(false, ModelComparisonVote.TIE, ModelComparisonModelChoice.TIE)] + public void TheDecisionNamesAModelOfTheSeriesNotAChat(bool firstModelIsChatOne, ModelComparisonVote vote, ModelComparisonModelChoice expected) + { + var task = DecidingTask(firstModelIsChatOne); + + using (Assert.EnterMultipleScope()) + { + Assert.That(task.Decide(vote, DECIDED, isBlind: true), Is.True); + Assert.That(task.Vote, Is.EqualTo(expected)); + Assert.That(task.State, Is.EqualTo(ModelComparisonSeriesTaskState.DONE)); + Assert.That(task.FinishedAtUtc, Is.EqualTo(DECIDED)); + } + } + + [Test] + public void ADecisionAfterTheResultWasShownIsNotBlind() + { + var task = DecidingTask(firstModelIsChatOne: true); + + task.Decide(ModelComparisonVote.COLUMN_A, DECIDED, isBlind: false); + + Assert.That(task.IsBlind, Is.False); + } + + [Test] + public void AStepOutOfOrderChangesNothing() + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne: true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(task.FinishSecondChat(SECOND_SENT, SECOND_DONE, 1), Is.False); + Assert.That(task.Decide(ModelComparisonVote.COLUMN_A, DECIDED, isBlind: true), Is.False); + Assert.That(task.State, Is.EqualTo(ModelComparisonSeriesTaskState.FIRST_CHAT)); + Assert.That(task.Vote, Is.Null); + Assert.That(task.ChatTwo.FinishedAtUtc, Is.Null); + } + + var decided = DecidingTask(firstModelIsChatOne: true); + decided.Decide(ModelComparisonVote.COLUMN_B, DECIDED, isBlind: true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(decided.FinishFirstChat(FIRST_SENT, FIRST_DONE, 5, "other"), Is.False); + Assert.That(decided.Decide(ModelComparisonVote.COLUMN_A, DECIDED.AddMinutes(1), isBlind: false), Is.False); + Assert.That(decided.Vote, Is.EqualTo(ModelComparisonModelChoice.SECOND)); + Assert.That(decided.IsBlind, Is.True); + } + } + + [TestCase(true, ModelComparisonVote.COLUMN_A, ModelComparisonModelChoice.FIRST, 1)] + [TestCase(true, ModelComparisonVote.COLUMN_B, ModelComparisonModelChoice.SECOND, 2)] + [TestCase(false, ModelComparisonVote.COLUMN_A, ModelComparisonModelChoice.SECOND, 1)] + [TestCase(false, ModelComparisonVote.COLUMN_B, ModelComparisonModelChoice.FIRST, 2)] + public void TheVerdictOfTheJudgeNamesAModelOfTheSeriesAndItsChatAgain(bool firstModelIsChatOne, ModelComparisonVote judgeVote, ModelComparisonModelChoice expectedChoice, int expectedChatNumber) + { + var task = DecidingTask(firstModelIsChatOne); + task.Decide(ModelComparisonVote.TIE, DECIDED, isBlind: true); + + task.TakeVerdict(new ModelComparisonJudgeVerdict { Label = "judge", Completed = true, Preferred = judgeVote, Reasoning = "Chat 1 was right." }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(task.JudgeStatus, Is.EqualTo(ModelComparisonSeriesJudgeStatus.VERDICT)); + Assert.That(task.JudgeChoice, Is.EqualTo(expectedChoice)); + Assert.That(task.ChatNumberOf(expectedChoice), Is.EqualTo(expectedChatNumber)); + Assert.That(task.JudgeReasoning, Is.EqualTo("Chat 1 was right.")); + } + } + + [Test] + public void AJudgeWithoutAUsableVerdictLeavesNoChoice() + { + var task = DecidingTask(firstModelIsChatOne: true); + task.Decide(ModelComparisonVote.COLUMN_A, DECIDED, isBlind: true); + task.TakeVerdict(new ModelComparisonJudgeVerdict { Label = "judge", Completed = true, Preferred = ModelComparisonVote.COLUMN_B, Reasoning = "first try" }); + + task.TakeVerdict(new ModelComparisonJudgeVerdict { Label = "judge", Completed = false }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(task.JudgeStatus, Is.EqualTo(ModelComparisonSeriesJudgeStatus.NO_VERDICT)); + Assert.That(task.JudgeChoice, Is.Null); + Assert.That(task.JudgeReasoning, Is.Empty); + } + + task.SkipJudge(ModelComparisonSeriesJudgeStatus.TOO_LONG); + Assert.That(task.JudgeStatus, Is.EqualTo(ModelComparisonSeriesJudgeStatus.TOO_LONG)); + } + + private static ModelComparisonSeriesTask DecidingTask(bool firstModelIsChatOne) + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne); + task.FinishFirstChat(FIRST_SENT, FIRST_DONE, 1, "question"); + task.FinishSecondChat(SECOND_SENT, SECOND_DONE, 1); + return task; + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesThreadsTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesThreadsTests.cs new file mode 100644 index 000000000..4b41bfdcd --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonSeriesThreadsTests.cs @@ -0,0 +1,89 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Chat; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks that both chats of a task of a comparison series get the same setup. +/// +[TestFixture] +public sealed class ModelComparisonSeriesThreadsTests +{ + [Test] + public void ANewChatHasTheSetupOfTheSeries() + { + var thread = ModelComparisonSeriesThreads.Create(Series(), "Weekly reports"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(thread.Name, Is.EqualTo("Weekly reports")); + Assert.That(thread.WorkspaceId, Is.EqualTo(Guid.Empty)); + Assert.That(thread.Blocks, Is.Empty); + Assert.That(thread.SystemPrompt, Is.EqualTo(ModelComparisonSeriesThreads.SYSTEM_PROMPT)); + Assert.That(thread.SelectedProfile, Is.EqualTo("profile")); + Assert.That(thread.SelectedChatTemplate, Is.EqualTo(ChatTemplate.NO_CHAT_TEMPLATE.Id)); + Assert.That(thread.SelectedToolIds, Is.EquivalentTo(new[] { "web_search" })); + Assert.That(thread.DataSourceOptions.DisableDataSources, Is.False); + Assert.That(thread.DataSourceOptions.PreselectedDataSourceIds, Is.EquivalentTo(new[] { "handbook" })); + Assert.That(thread.DataSourceOptions.RetrievalMode, Is.EqualTo(DataSourceRetrievalMode.EVERY_MESSAGE)); + } + } + + [Test] + public void TheSystemPromptAsksTheModelNotToNameItself() + { + Assert.That(ModelComparisonSeriesThreads.SYSTEM_PROMPT, Does.StartWith(SystemPrompts.DEFAULT).And.Contains("Do not state your name")); + } + + [Test] + public void ARequestGetsTheSetupOfTheSeriesBack() + { + var series = Series(); + var thread = ModelComparisonSeriesThreads.Create(series, "chat"); + + // What a request could have changed, e.g., a stored chat of an older version: + thread.SystemPrompt = "something else"; + thread.SelectedProfile = "another profile"; + thread.DataSourceOptions = new DataSourceOptions { DisableDataSources = true, AutomaticDataSourceSelection = true }; + + ModelComparisonSeriesThreads.PrepareForRequest(thread, series, AIStudio.Settings.Provider.NONE, ["web_search"]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(thread.SystemPrompt, Is.EqualTo(ModelComparisonSeriesThreads.SYSTEM_PROMPT)); + Assert.That(thread.SystemPromptOrigin, Is.Null); + Assert.That(thread.SelectedProvider, Is.EqualTo(AIStudio.Settings.Provider.NONE.Id)); + Assert.That(thread.SelectedProfile, Is.EqualTo("profile")); + Assert.That(thread.IncludeDateTime, Is.True); + Assert.That(thread.RuntimeSelectedToolIds, Is.EquivalentTo(new[] { "web_search" })); + Assert.That(thread.RuntimeToolsAreAssistantManaged, Is.True); + Assert.That(thread.DataSourceOptions.DisableDataSources, Is.False); + Assert.That(thread.DataSourceOptions.AutomaticDataSourceSelection, Is.False); + Assert.That(thread.DataSourceOptions.AutomaticValidation, Is.False); + } + } + + [Test] + public void ASeriesWithoutDataSourcesSearchesNone() + { + var thread = ModelComparisonSeriesThreads.Create(Series() with { DataSourceIds = [], RetrievalMode = null }, "chat"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(thread.DataSourceOptions.DisableDataSources, Is.True); + Assert.That(thread.DataSourceOptions.PreselectedDataSourceIds, Is.Empty); + } + } + + private static ModelComparisonSeries Series() => new() + { + Id = Guid.NewGuid(), + Name = "Weekly reports", + ProfileId = "profile", + ToolIds = ["web_search"], + DataSourceIds = ["handbook"], + RetrievalMode = DataSourceRetrievalMode.EVERY_MESSAGE, + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonShownChatTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonShownChatTests.cs new file mode 100644 index 000000000..ad967fce5 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonShownChatTests.cs @@ -0,0 +1,61 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks which chat of a task the user works in, which model answers there, and whether it starts with the first +/// message of the first chat. +/// +[TestFixture] +public sealed class ModelComparisonShownChatTests +{ + private static readonly DateTimeOffset BEGUN = new(2026, 11, 13, 9, 0, 0, TimeSpan.Zero); + + private static readonly AIStudio.Settings.Provider FIRST_PROVIDER = new(0, "11111111-1111-1111-1111-111111111111", "First", LLMProviders.OPEN_AI, new Model("model-a", null)); + + private static readonly AIStudio.Settings.Provider SECOND_PROVIDER = new(1, "22222222-2222-2222-2222-222222222222", "Second", LLMProviders.MISTRAL, new Model("model-b", null)); + + [TestCase(true, "First")] + [TestCase(false, "Second")] + public void TheFirstChatHasTheModelDrawnForItAndNoOpeningMessage(bool firstModelIsChatOne, string expectedProvider) + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne); + var chat = ModelComparisonShownChat.Of(task, FIRST_PROVIDER, SECOND_PROVIDER); + + Assert.That(chat, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(chat!.ChatId, Is.EqualTo(task.ChatOne.ChatId)); + Assert.That(chat.Provider.InstanceName, Is.EqualTo(expectedProvider)); + Assert.That(chat.OpensWithFirstMessage, Is.False, "The first chat starts with whatever the user writes."); + } + } + + [TestCase(true, "Second")] + [TestCase(false, "First")] + public void TheSecondChatHasTheOtherModelAndStartsWithTheFirstMessage(bool firstModelIsChatOne, string expectedProvider) + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne); + task.FinishFirstChat(BEGUN.AddMinutes(1), BEGUN.AddMinutes(5), 1, "What is new today?"); + var chat = ModelComparisonShownChat.Of(task, FIRST_PROVIDER, SECOND_PROVIDER); + + Assert.That(chat, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(chat!.ChatId, Is.EqualTo(task.ChatTwo.ChatId)); + Assert.That(chat.Provider.InstanceName, Is.EqualTo(expectedProvider), "The second chat must never go to the model of the first one."); + Assert.That(chat.OpensWithFirstMessage, Is.True, "Without the first message, the second model would never answer the task."); + } + } + + [Test] + public void ATaskWaitingForTheDecisionShowsNoChat() + { + var task = ModelComparisonSeriesTask.Begin(BEGUN, firstModelIsChatOne: true); + task.FinishFirstChat(BEGUN.AddMinutes(1), BEGUN.AddMinutes(5), 1, "What is new today?"); + task.FinishSecondChat(BEGUN.AddMinutes(6), BEGUN.AddMinutes(9), 1); + + Assert.That(ModelComparisonShownChat.Of(task, FIRST_PROVIDER, SECOND_PROVIDER), Is.Null); + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonStatisticsTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonStatisticsTests.cs new file mode 100644 index 000000000..dc0ea4a76 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonStatisticsTests.cs @@ -0,0 +1,274 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks what the statistics make of the stored comparisons. +/// +[TestFixture] +public sealed class ModelComparisonStatisticsTests +{ + private static readonly DateTimeOffset EARLIER = new(2026, 11, 1, 9, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset LATER = new(2026, 11, 13, 9, 0, 0, TimeSpan.Zero); + + // OPEN_AI comes before MISTRAL by ModelComparisonModelKey.CompareTo, so A is the first model of their pair: + private static readonly ModelComparisonModelIdentity A = new() { LLMProvider = LLMProviders.OPEN_AI, ModelId = "model-a", Label = "Model A" }; + private static readonly ModelComparisonModelIdentity B = new() { LLMProvider = LLMProviders.MISTRAL, ModelId = "model-b", Label = "Model B" }; + private static readonly ModelComparisonModelIdentity JUDGE = new() { LLMProvider = LLMProviders.ANTHROPIC, ModelId = "judge", Label = "Judge" }; + + [Test] + public async Task NoStoredComparisonsGiveEmptyStatistics() + { + var statistics = await ComputeAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.ComparisonCount, Is.Zero); + Assert.That(statistics.RunCount, Is.Zero); + Assert.That(statistics.VoteRanking, Is.Empty); + Assert.That(statistics.JudgeRanking, Is.Empty); + Assert.That(statistics.Pairings, Is.Empty); + Assert.That(statistics.JudgeAgreements, Is.Empty); + Assert.That(statistics.Speeds, Is.Empty); + } + } + + [Test] + public async Task TheVotesRankTheModels() + { + var statistics = await ComputeAsync(Record(A, B, LATER, judge: null, + Run(ModelComparisonModelChoice.FIRST), + Run(ModelComparisonModelChoice.FIRST), + Run(ModelComparisonModelChoice.SECOND), + Run(ModelComparisonModelChoice.TIE), + Run(vote: null))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.ComparisonCount, Is.EqualTo(1)); + Assert.That(statistics.RunCount, Is.EqualTo(5)); + Assert.That(statistics.VoteRanking.Select(standing => standing.Model.Label), Is.EqualTo(new[] { "Model A", "Model B" })); + Assert.That(statistics.VoteRanking[0].Wins, Is.EqualTo(2)); + Assert.That(statistics.VoteRanking[0].Losses, Is.EqualTo(1)); + Assert.That(statistics.VoteRanking[0].Ties, Is.EqualTo(1)); + Assert.That(statistics.VoteRanking[1].Wins, Is.EqualTo(1)); + Assert.That(statistics.VoteRanking[1].Losses, Is.EqualTo(2)); + Assert.That(statistics.JudgeRanking, Is.Empty, "No judge took part."); + } + } + + [Test] + public async Task TheVerdictsOfTheJudgeRankTheModelsOnTheirOwn() + { + var statistics = await ComputeAsync(Record(A, B, LATER, JUDGE, + Run(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.SECOND), + Run(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.SECOND))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.VoteRanking[0].Model.Label, Is.EqualTo("Model A")); + Assert.That(statistics.JudgeRanking[0].Model.Label, Is.EqualTo("Model B")); + Assert.That(statistics.JudgeRanking[0].Wins, Is.EqualTo(2)); + } + } + + [Test] + public async Task APairCountsTheSameWhicheverModelWasPickedFirst() + { + // Model A wins both runs, once as the first model and once as the second one: + var statistics = await ComputeAsync( + Record(A, B, EARLIER, judge: null, Run(ModelComparisonModelChoice.FIRST)), + Record(B, A, LATER, judge: null, Run(ModelComparisonModelChoice.SECOND))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.Pairings, Has.Count.EqualTo(1)); + Assert.That(statistics.Pairings[0].First.Label, Is.EqualTo("Model A")); + Assert.That(statistics.Pairings[0].Second.Label, Is.EqualTo("Model B")); + Assert.That(statistics.Pairings[0].ComparisonCount, Is.EqualTo(2)); + Assert.That(statistics.Pairings[0].Votes.FirstModelWins, Is.EqualTo(2)); + Assert.That(statistics.Pairings[0].Votes.SecondModelWins, Is.Zero); + Assert.That(statistics.Pairings[0].WinShareOfFirstByVotes.Share, Is.EqualTo(1.0)); + } + } + + [Test] + public async Task AComparisonOfAModelWithItselfRanksNothing() + { + var statistics = await ComputeAsync(Record(A, A with { Label = "Model A again" }, LATER, judge: null, + Run(ModelComparisonModelChoice.FIRST, firstTotalSeconds: 2, secondTotalSeconds: 4))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.VoteRanking, Is.Empty); + Assert.That(statistics.Pairings, Is.Empty); + Assert.That(statistics.Speeds, Has.Count.EqualTo(1), "Both answers came from the same model."); + Assert.That(statistics.Speeds[0].AnswerCount, Is.EqualTo(2)); + Assert.That(statistics.Speeds[0].MedianTotalTime, Is.EqualTo(TimeSpan.FromSeconds(3))); + } + } + + [Test] + public async Task AJudgeAgreesWhenItChoosesTheSameAsTheUser() + { + var statistics = await ComputeAsync(Record(A, B, LATER, JUDGE, + Run(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.FIRST), + Run(ModelComparisonModelChoice.TIE, ModelComparisonModelChoice.FIRST), + Run(ModelComparisonModelChoice.TIE, ModelComparisonModelChoice.TIE), + Run(vote: null, ModelComparisonModelChoice.SECOND))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.JudgeAgreements, Has.Count.EqualTo(1)); + Assert.That(statistics.JudgeAgreements[0].Judge.Label, Is.EqualTo("Judge")); + Assert.That(statistics.JudgeAgreements[0].Agreements, Is.EqualTo(2)); + Assert.That(statistics.JudgeAgreements[0].ComparedRuns, Is.EqualTo(3), "A run without a vote has nothing to agree with."); + Assert.That(statistics.JudgeAgreements[0].Share, Is.EqualTo(2.0 / 3).Within(0.0001)); + } + } + + [Test] + public async Task TheSpeedIsTheMedianOfTheCompleteAnswers() + { + var statistics = await ComputeAsync(Record(A, B, LATER, judge: null, + Run(ModelComparisonModelChoice.FIRST, firstTotalSeconds: 1, secondTotalSeconds: 20), + Run(ModelComparisonModelChoice.FIRST, firstTotalSeconds: 3, secondTotalSeconds: null), + Run(ModelComparisonModelChoice.FIRST, firstTotalSeconds: 60, secondTotalSeconds: 30))); + + var speedOfA = statistics.Speeds.Single(speed => speed.Model.Label == "Model A"); + var speedOfB = statistics.Speeds.Single(speed => speed.Model.Label == "Model B"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.Speeds[0].Model.Label, Is.EqualTo("Model A"), "The fastest model comes first."); + Assert.That(speedOfA.AnswerCount, Is.EqualTo(3)); + Assert.That(speedOfA.MedianTotalTime, Is.EqualTo(TimeSpan.FromSeconds(3)), "One slow answer does not shift the median."); + Assert.That(speedOfB.AnswerCount, Is.EqualTo(2), "An answer which never came does not count."); + Assert.That(speedOfB.MedianTotalTime, Is.EqualTo(TimeSpan.FromSeconds(25))); + } + } + + [Test] + public async Task AModelIsShownWithTheNameOfItsLatestComparison() + { + // The later comparison comes first, as the files may come in any order: + var statistics = await ComputeAsync( + Record(A with { Label = "Renamed model A" }, B, LATER, judge: null, Run(ModelComparisonModelChoice.FIRST)), + Record(A, B, EARLIER, judge: null, Run(ModelComparisonModelChoice.FIRST))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.VoteRanking[0].Model.Label, Is.EqualTo("Renamed model A")); + Assert.That(statistics.VoteRanking[0].Wins, Is.EqualTo(2), "Both comparisons count for the same model."); + } + } + + [Test] + public async Task OnlyTheBlindTasksOfASeriesWithAShownResultCount() + { + var resolved = Series(A, B, JUDGE, isResolved: true, + SeriesTask(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.FIRST), + SeriesTask(ModelComparisonModelChoice.FIRST, ModelComparisonModelChoice.SECOND), + SeriesTask(ModelComparisonModelChoice.SECOND, isBlind: false), + SeriesTask(vote: null, state: ModelComparisonSeriesTaskState.DECIDING)); + var unresolved = Series(A, B, judge: null, isResolved: false, SeriesTask(ModelComparisonModelChoice.SECOND)); + + var statistics = await ModelComparisonStatistics.ComputeAsync(ToAsync([]), [resolved, unresolved], ModelComparisonResultSource.ALL); + + using (Assert.EnterMultipleScope()) + { + Assert.That(statistics.SeriesCount, Is.EqualTo(1)); + Assert.That(statistics.TaskCount, Is.EqualTo(2)); + Assert.That(statistics.IsEmpty, Is.False); + Assert.That(statistics.Pairings.Single().Votes.FirstModelWins, Is.EqualTo(2)); + Assert.That(statistics.Pairings.Single().Votes.SecondModelWins, Is.Zero); + Assert.That(statistics.Pairings.Single().ComparisonCount, Is.EqualTo(1)); + Assert.That(statistics.JudgeAgreements.Single().ComparedRuns, Is.EqualTo(2)); + Assert.That(statistics.JudgeAgreements.Single().Agreements, Is.EqualTo(1)); + Assert.That(statistics.Speeds, Is.Empty); + } + } + + [Test] + public async Task TheSourceChoosesWhichResultsCount() + { + var record = Record(A, B, LATER, judge: null, Run(ModelComparisonModelChoice.FIRST)); + var series = Series(A, B, judge: null, isResolved: true, SeriesTask(ModelComparisonModelChoice.SECOND)); + + var all = await ModelComparisonStatistics.ComputeAsync(ToAsync([record]), [series], ModelComparisonResultSource.ALL); + var answers = await ModelComparisonStatistics.ComputeAsync(ToAsync([record]), [series], ModelComparisonResultSource.ANSWERS); + var tasks = await ModelComparisonStatistics.ComputeAsync(ToAsync([record]), [series], ModelComparisonResultSource.TASKS); + + using (Assert.EnterMultipleScope()) + { + Assert.That(all.Pairings.Single().Votes.FirstModelWins, Is.EqualTo(1)); + Assert.That(all.Pairings.Single().Votes.SecondModelWins, Is.EqualTo(1)); + Assert.That(all.Pairings.Single().ComparisonCount, Is.EqualTo(2)); + + Assert.That(answers.SeriesCount, Is.Zero); + Assert.That(answers.Pairings.Single().Votes.SecondModelWins, Is.Zero); + + Assert.That(tasks.ComparisonCount, Is.Zero); + Assert.That(tasks.Pairings.Single().Votes.FirstModelWins, Is.Zero); + Assert.That(tasks.Pairings.Single().Votes.SecondModelWins, Is.EqualTo(1)); + } + } + + private static Task ComputeAsync(params ModelComparisonRecord[] records) => ModelComparisonStatistics.ComputeAsync(ToAsync(records)); + + private static ModelComparisonSeries Series(ModelComparisonModelIdentity first, ModelComparisonModelIdentity second, ModelComparisonModelIdentity? judge, bool isResolved, params ModelComparisonSeriesTask[] tasks) => new() + { + Id = Guid.NewGuid(), + Name = "Series", + CreatedAtUtc = EARLIER, + First = first, + Second = second, + Judge = judge, + IsResolved = isResolved, + ResolvedAtUtc = isResolved ? LATER : null, + Tasks = [.. tasks], + }; + + private static ModelComparisonSeriesTask SeriesTask(ModelComparisonModelChoice? vote, ModelComparisonModelChoice? judgeChoice = null, bool isBlind = true, ModelComparisonSeriesTaskState state = ModelComparisonSeriesTaskState.DONE) => new() + { + Id = Guid.NewGuid(), + StartedAtUtc = EARLIER, + State = state, + Vote = vote, + JudgeChoice = judgeChoice, + IsBlind = isBlind, + }; + + private static async IAsyncEnumerable ToAsync(IEnumerable records) + { + foreach (var record in records) + { + await Task.Yield(); + yield return record; + } + } + + private static ModelComparisonRecord Record(ModelComparisonModelIdentity first, ModelComparisonModelIdentity second, DateTimeOffset finishedAtUtc, ModelComparisonModelIdentity? judge, params ModelComparisonRecordRun[] runs) => new() + { + Id = Guid.NewGuid(), + FinishedAtUtc = finishedAtUtc, + QuestionPreview = "Summarize it.", + First = first, + Second = second, + Judge = judge, + Runs = [.. runs], + }; + + private static ModelComparisonRecordRun Run(ModelComparisonModelChoice? vote, ModelComparisonModelChoice? judgeChoice = null, double? firstTotalSeconds = 1, double? secondTotalSeconds = 1) => new() + { + Vote = vote, + JudgeChoice = judgeChoice, + First = Answer(firstTotalSeconds), + Second = Answer(secondTotalSeconds), + }; + + private static ModelComparisonRecordAnswer Answer(double? totalSeconds) => totalSeconds is { } seconds + ? new ModelComparisonRecordAnswer { Answered = true, FirstTokenTime = TimeSpan.FromSeconds(seconds / 2), TotalTime = TimeSpan.FromSeconds(seconds) } + : new ModelComparisonRecordAnswer { Answered = false }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonStorePresetTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonStorePresetTests.cs new file mode 100644 index 000000000..cdfb0093a --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonStorePresetTests.cs @@ -0,0 +1,168 @@ +using AIStudio.Assistants.ModelComparison; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the Model Comparison assistant stores its presets as files below the data directory. +/// +[TestFixture] +public sealed class ModelComparisonStorePresetTests +{ + private string dataDirectory = string.Empty; + + private string PresetsDirectory => Path.Combine(this.dataDirectory, ModelComparisonStore.ROOT_DIRECTORY_NAME, "presets"); + + [SetUp] + public void CreateDataDirectory() + { + this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-model-comparison-{Guid.NewGuid():N}"); + Directory.CreateDirectory(this.dataDirectory); + } + + [TearDown] + public void DeleteDataDirectory() + { + if (Directory.Exists(this.dataDirectory)) + Directory.Delete(this.dataDirectory, true); + } + + [Test] + public async Task ASavedPresetIsListedWithAllItsFields() + { + var store = this.CreateStore(); + var preset = Preset(" Summaries "); + + await store.SavePresetAsync(preset); + var presets = await store.ListPresetsAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(presets, Has.Count.EqualTo(1)); + Assert.That(presets[0], Is.EqualTo(preset with { Name = "Summaries" }), "The name is trimmed, everything else stays as it was."); + Assert.That(File.Exists(Path.Combine(this.PresetsDirectory, $"{preset.Id:D}.json")), Is.True); + } + } + + [Test] + public async Task ANewStoreFindsThePresetsOfTheLastOne() + { + var preset = Preset("Summaries"); + await this.CreateStore().SavePresetAsync(preset); + + var presets = await this.CreateStore().ListPresetsAsync(); + + Assert.That(presets.Select(listed => listed.Id), Is.EqualTo(new[] { preset.Id })); + } + + [Test] + public async Task SavingAgainReplacesThePreset() + { + var store = this.CreateStore(); + var preset = Preset("Summaries"); + await store.SavePresetAsync(preset); + + await store.SavePresetAsync(preset with { Name = "Translations", RunCount = 10 }); + var presets = await store.ListPresetsAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(presets, Has.Count.EqualTo(1)); + Assert.That(presets[0].Name, Is.EqualTo("Translations")); + Assert.That(presets[0].RunCount, Is.EqualTo(10)); + } + } + + [Test] + public async Task ThePresetsAreSortedByName() + { + var store = this.CreateStore(); + await store.SavePresetAsync(Preset("Translations")); + await store.SavePresetAsync(Preset("abstracts")); + await store.SavePresetAsync(Preset("Summaries")); + + var presets = await store.ListPresetsAsync(); + + Assert.That(presets.Select(preset => preset.Name), Is.EqualTo(new[] { "abstracts", "Summaries", "Translations" })); + } + + [Test] + public async Task ADeletedPresetIsGone() + { + var store = this.CreateStore(); + var kept = Preset("Kept"); + var deleted = Preset("Deleted"); + await store.SavePresetAsync(kept); + await store.SavePresetAsync(deleted); + + await store.DeletePresetAsync(deleted.Id); + var presets = await store.ListPresetsAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(presets.Select(preset => preset.Id), Is.EqualTo(new[] { kept.Id })); + Assert.That(File.Exists(Path.Combine(this.PresetsDirectory, $"{deleted.Id:D}.json")), Is.False); + } + } + + [Test] + public async Task APresetThisVersionCannotReadIsLeftOutButKept() + { + var store = this.CreateStore(); + var readable = Preset("Readable"); + await store.SavePresetAsync(readable); + + // A newer version may write a field this one does not know: + var unreadablePath = Path.Combine(this.PresetsDirectory, $"{Guid.NewGuid():D}.json"); + await File.WriteAllTextAsync(unreadablePath, """{ "name": "From a newer version", "fieldOfANewerVersion": true }"""); + + var presets = await store.ListPresetsAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(presets.Select(preset => preset.Id), Is.EqualTo(new[] { readable.Id })); + Assert.That(File.Exists(unreadablePath), Is.True); + } + } + + [Test] + public async Task TheFileNameDecidesTheIdOfAPreset() + { + var store = this.CreateStore(); + var preset = Preset("Summaries"); + await store.SavePresetAsync(preset); + + // A copied file keeps the ID of its original inside; its own name is what a save or delete has to reach: + var copyId = Guid.NewGuid(); + File.Copy(Path.Combine(this.PresetsDirectory, $"{preset.Id:D}.json"), Path.Combine(this.PresetsDirectory, $"{copyId:D}.json")); + + var presets = await store.ListPresetsAsync(); + + Assert.That(presets.Select(listed => listed.Id), Is.EquivalentTo(new[] { preset.Id, copyId })); + } + + [Test] + public async Task APresetWithoutANameIsRefused() + { + var store = this.CreateStore(); + + await Assert.ThrowsAsync(() => store.SavePresetAsync(Preset(" "))); + } + + private ModelComparisonStore CreateStore() => new(NullLogger.Instance, new ModelComparisonStorageOptions { DataDirectory = this.dataDirectory }); + + private static ModelComparisonPreset Preset(string name) => new() + { + Id = Guid.NewGuid(), + Name = name, + FirstProviderId = "first", + SecondProviderId = "second", + JudgeEnabled = true, + JudgeProviderId = "judge", + JudgeInstructions = "Keep every number.", + Context = "The document, with umlauts: Größe.", + Question = "Summarize it.", + RunCount = 5, + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonStoreResultTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonStoreResultTests.cs new file mode 100644 index 000000000..eb21c6466 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonStoreResultTests.cs @@ -0,0 +1,163 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Provider; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the Model Comparison assistant stores the results of its batches below the data directory. +/// +[TestFixture] +public sealed class ModelComparisonStoreResultTests +{ + private string dataDirectory = string.Empty; + + private string ResultsDirectory => Path.Combine(this.dataDirectory, ModelComparisonStore.ROOT_DIRECTORY_NAME, "results"); + + [SetUp] + public void CreateDataDirectory() + { + this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-model-comparison-{Guid.NewGuid():N}"); + Directory.CreateDirectory(this.dataDirectory); + } + + [TearDown] + public void DeleteDataDirectory() + { + if (Directory.Exists(this.dataDirectory)) + Directory.Delete(this.dataDirectory, true); + } + + [Test] + public async Task AStoredComparisonIsReadBackWithAllItsFields() + { + var record = Record(); + await this.CreateStore().SaveResultAsync(record); + + var records = await ReadAllAsync(this.CreateStore()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(records, Has.Count.EqualTo(1)); + Assert.That(records[0].Id, Is.EqualTo(record.Id)); + Assert.That(records[0].FinishedAtUtc, Is.EqualTo(record.FinishedAtUtc)); + Assert.That(records[0].QuestionPreview, Is.EqualTo(record.QuestionPreview)); + Assert.That(records[0].First, Is.EqualTo(record.First)); + Assert.That(records[0].Second, Is.EqualTo(record.Second)); + Assert.That(records[0].Judge, Is.EqualTo(record.Judge)); + Assert.That(records[0].Runs, Is.EqualTo(record.Runs)); + } + } + + [Test] + public async Task StoringTheSameBatchAgainDoesNotCountItTwice() + { + var store = this.CreateStore(); + var record = Record(); + + await store.SaveResultAsync(record); + await store.SaveResultAsync(record with { QuestionPreview = "Summarize it again." }); + var records = await ReadAllAsync(store); + + using (Assert.EnterMultipleScope()) + { + Assert.That(records, Has.Count.EqualTo(1)); + Assert.That(records[0].QuestionPreview, Is.EqualTo("Summarize it again.")); + } + } + + [Test] + public async Task AFileThisVersionCannotReadIsSkippedButKept() + { + var store = this.CreateStore(); + await store.SaveResultAsync(Record()); + + var unreadablePath = Path.Combine(this.ResultsDirectory, $"{Guid.NewGuid():D}.json"); + await File.WriteAllTextAsync(unreadablePath, """{ "fieldOfANewerVersion": true }"""); + + var records = await ReadAllAsync(store); + + using (Assert.EnterMultipleScope()) + { + Assert.That(records, Has.Count.EqualTo(1)); + Assert.That(File.Exists(unreadablePath), Is.True); + } + } + + [Test] + public async Task DeletingAllResultsKeepsThePresets() + { + var store = this.CreateStore(); + await store.SaveResultAsync(Record()); + await store.SaveResultAsync(Record()); + await store.SavePresetAsync(new ModelComparisonPreset { Id = Guid.NewGuid(), Name = "Weekly report" }); + + var unreadablePath = Path.Combine(this.ResultsDirectory, $"{Guid.NewGuid():D}.json"); + await File.WriteAllTextAsync(unreadablePath, """{ "fieldOfANewerVersion": true }"""); + + await store.DeleteAllResultsAsync(); + + var records = await ReadAllAsync(this.CreateStore()); + var presets = await this.CreateStore().ListPresetsAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(records, Is.Empty); + Assert.That(File.Exists(unreadablePath), Is.False, "Deleting all results takes the files this version cannot read along."); + Assert.That(presets, Has.Count.EqualTo(1)); + } + } + + [Test] + public async Task DeletingAllResultsWithoutAnyDoesNothing() + { + await this.CreateStore().DeleteAllResultsAsync(); + + Assert.That(await ReadAllAsync(this.CreateStore()), Is.Empty); + } + + [Test] + public async Task NoStoredComparisonsReadAsNone() + { + var records = await ReadAllAsync(this.CreateStore()); + + Assert.That(records, Is.Empty); + } + + private ModelComparisonStore CreateStore() => new(NullLogger.Instance, new ModelComparisonStorageOptions { DataDirectory = this.dataDirectory }); + + private static async Task> ReadAllAsync(ModelComparisonStore store) + { + List records = []; + await foreach (var record in store.ReadResultsAsync()) + records.Add(record); + + return records; + } + + private static ModelComparisonRecord Record() => new() + { + Id = Guid.NewGuid(), + FinishedAtUtc = new DateTimeOffset(2026, 11, 13, 9, 30, 0, TimeSpan.Zero), + QuestionPreview = "Summarize it.", + First = new() { LLMProvider = LLMProviders.OPEN_AI, ModelId = "first-model", Label = "First" }, + Second = new() { LLMProvider = LLMProviders.SELF_HOSTED, Hostname = "http://gpu-server:8080", ModelId = "second-model", Label = "Second" }, + Judge = new() { LLMProvider = LLMProviders.MISTRAL, ModelId = "judge-model", Label = "Judge" }, + Runs = + [ + new() + { + Vote = ModelComparisonModelChoice.FIRST, + JudgeChoice = ModelComparisonModelChoice.TIE, + First = new() { Answered = true, FirstTokenTime = TimeSpan.FromMilliseconds(800), TotalTime = TimeSpan.FromSeconds(5) }, + Second = new() { Answered = true, FirstTokenTime = TimeSpan.FromSeconds(1), TotalTime = TimeSpan.FromSeconds(7) }, + }, + new() + { + First = new() { Answered = true, FirstTokenTime = TimeSpan.FromSeconds(1), TotalTime = TimeSpan.FromSeconds(4) }, + Second = new() { Answered = false }, + }, + ], + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonStoreSeriesTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonStoreSeriesTests.cs new file mode 100644 index 000000000..a214db7f3 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonStoreSeriesTests.cs @@ -0,0 +1,252 @@ +using AIStudio.Assistants.ModelComparison; +using AIStudio.Chat; +using AIStudio.Provider; +using AIStudio.Settings.DataModel; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the Model Comparison assistant stores its comparison series and their chats below the data directory. +/// +[TestFixture] +public sealed class ModelComparisonStoreSeriesTests +{ + private static readonly DateTimeOffset EARLIER = new(2026, 11, 1, 9, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset LATER = new(2026, 11, 13, 9, 0, 0, TimeSpan.Zero); + + private string dataDirectory = string.Empty; + + [SetUp] + public void CreateDataDirectory() + { + this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-model-comparison-{Guid.NewGuid():N}"); + Directory.CreateDirectory(this.dataDirectory); + } + + [TearDown] + public void DeleteDataDirectory() + { + if (Directory.Exists(this.dataDirectory)) + Directory.Delete(this.dataDirectory, true); + } + + [Test] + public async Task AStoredSeriesIsReadBackWithAllItsFields() + { + var series = Series(EARLIER); + await this.CreateStore().CreateSeriesAsync(series); + + var loaded = await this.CreateStore().LoadSeriesAsync(series.Id); + var task = loaded!.Tasks.Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(loaded.Name, Is.EqualTo(series.Name)); + Assert.That(loaded.First, Is.EqualTo(series.First)); + Assert.That(loaded.Judge, Is.EqualTo(series.Judge)); + Assert.That(loaded.JudgeInstructions, Is.EqualTo("I prefer short answers.")); + Assert.That(loaded.ToolIds, Is.EqualTo(series.ToolIds)); + Assert.That(loaded.DataSourceIds, Is.EqualTo(series.DataSourceIds)); + Assert.That(loaded.RetrievalMode, Is.EqualTo(DataSourceRetrievalMode.SEMANTIC_SEARCH)); + Assert.That(task.State, Is.EqualTo(ModelComparisonSeriesTaskState.DONE)); + Assert.That(task.Vote, Is.EqualTo(ModelComparisonModelChoice.SECOND)); + Assert.That(task.ChatTwo.UserMessageCount, Is.EqualTo(3)); + Assert.That(task.ChatOfFirstModel, Is.EqualTo(task.ChatTwo), "The first model answered in the second chat."); + Assert.That(task.ChatOne.Duration, Is.EqualTo(TimeSpan.FromMinutes(5))); + Assert.That(task.IsBlind, Is.True); + } + } + + [Test] + public async Task TheSeriesWorkedOnLastComesFirst() + { + var store = this.CreateStore(); + var older = Series(EARLIER); + var newer = Series(LATER); + await store.CreateSeriesAsync(older); + await store.CreateSeriesAsync(newer); + + var series = await store.ListSeriesAsync(); + + Assert.That(series.Select(entry => entry.Id), Is.EqualTo(new[] { newer.Id, older.Id })); + } + + [Test] + public async Task AChatOfASeriesIsStoredAndLoaded() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + + var thread = new ChatThread { ChatId = Guid.NewGuid() }; + thread.Blocks.Add(new ContentBlock { Role = ChatRole.USER, ContentType = ContentType.TEXT, Content = new ContentText { Text = "Summarize the report." } }); + await new ModelComparisonSeriesChatStore(store, series.Id).SaveThreadAsync(thread); + + var loaded = await new ModelComparisonSeriesChatStore(this.CreateStore(), series.Id).LoadThreadAsync(thread.ChatId); + + using (Assert.EnterMultipleScope()) + { + Assert.That(loaded!.Blocks, Has.Count.EqualTo(1)); + Assert.That(((ContentText)loaded.Blocks[0].Content!).Text, Is.EqualTo("Summarize the report.")); + Assert.That(store.ReadAllSeriesChatLocations()[thread.ChatId], Is.EqualTo(series.Id)); + } + } + + [Test] + public async Task AChatWithoutAFileLoadsAsNone() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + + Assert.That(await store.LoadSeriesChatAsync(series.Id, Guid.NewGuid()), Is.Null); + } + + [Test] + public async Task AChatOfADeletedSeriesIsNotStored() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + await store.DeleteSeriesAsync(series.Id); + + await Assert.ThrowsAsync(() => store.SaveSeriesChatAsync(series.Id, new ChatThread { ChatId = Guid.NewGuid() })); + Assert.That(Directory.Exists(Path.Combine(this.dataDirectory, ModelComparisonStore.ROOT_DIRECTORY_NAME, "series", series.Id.ToString("D"))), Is.False, "The series does not come back with a chat alone."); + } + + [Test] + public async Task SavingASeriesReplacesWhatWasStoredOfIt() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + + series.Name = "Monthly reports"; + await store.SaveSeriesAsync(series); + + Assert.That((await this.CreateStore().LoadSeriesAsync(series.Id))!.Name, Is.EqualTo("Monthly reports")); + } + + [Test] + public async Task ADeletedSeriesDoesNotComeBackWhenItIsSaved() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + await store.DeleteSeriesAsync(series.Id); + + await Assert.ThrowsAsync(() => store.SaveSeriesAsync(series)); + Assert.That(Directory.Exists(Path.Combine(this.dataDirectory, ModelComparisonStore.ROOT_DIRECTORY_NAME, "series", series.Id.ToString("D"))), Is.False, "E.g., the verdict of the judge must not bring a deleted series back."); + } + + [Test] + public async Task DeletingASeriesDeletesItsChats() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + var thread = new ChatThread { ChatId = Guid.NewGuid() }; + await store.SaveSeriesChatAsync(series.Id, thread); + + await store.DeleteSeriesAsync(series.Id); + + using (Assert.EnterMultipleScope()) + { + Assert.That(await store.ListSeriesAsync(), Is.Empty); + Assert.That(store.ReadAllSeriesChatLocations(), Is.Empty); + } + } + + [Test] + public async Task DeletingAllSeriesDeletesTheirChatsButKeepsThePresetsAndTheResults() + { + var store = this.CreateStore(); + var first = Series(EARLIER); + var second = Series(LATER); + await store.CreateSeriesAsync(first); + await store.CreateSeriesAsync(second); + await store.SaveSeriesChatAsync(first.Id, new ChatThread { ChatId = Guid.NewGuid() }); + await store.SavePresetAsync(new ModelComparisonPreset { Id = Guid.NewGuid(), Name = "Weekly report" }); + await store.SaveResultAsync(new ModelComparisonRecord { Id = Guid.NewGuid() }); + + var unreadableDirectory = Path.Combine(this.dataDirectory, ModelComparisonStore.ROOT_DIRECTORY_NAME, "series", Guid.NewGuid().ToString("D")); + Directory.CreateDirectory(unreadableDirectory); + await File.WriteAllTextAsync(Path.Combine(unreadableDirectory, "series.json"), """{ "fieldOfANewerVersion": true }"""); + + await store.DeleteAllSeriesAsync(); + + var results = 0; + await foreach (var _ in this.CreateStore().ReadResultsAsync()) + results++; + + using (Assert.EnterMultipleScope()) + { + Assert.That(await this.CreateStore().ListSeriesAsync(), Is.Empty); + Assert.That(store.ReadAllSeriesChatLocations(), Is.Empty); + Assert.That(Directory.Exists(unreadableDirectory), Is.False, "Deleting all series takes the series this version cannot read along."); + Assert.That(await this.CreateStore().ListPresetsAsync(), Has.Count.EqualTo(1)); + Assert.That(results, Is.EqualTo(1)); + } + } + + [Test] + public async Task DeletingAllSeriesWithoutAnyDoesNothing() + { + await this.CreateStore().DeleteAllSeriesAsync(); + + Assert.That(await this.CreateStore().ListSeriesAsync(), Is.Empty); + } + + [Test] + public async Task AChatThisVersionCannotOpenIsReportedAndKept() + { + var store = this.CreateStore(); + var series = Series(EARLIER); + await store.CreateSeriesAsync(series); + var chatId = Guid.NewGuid(); + var path = Path.Combine(this.dataDirectory, ModelComparisonStore.ROOT_DIRECTORY_NAME, "series", series.Id.ToString("D"), "chats", $"{chatId:D}.json"); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + await File.WriteAllTextAsync(path, "{ not json"); + + await Assert.ThrowsAsync(() => store.LoadSeriesChatAsync(series.Id, chatId)); + Assert.That(File.Exists(path), Is.True); + } + + private ModelComparisonStore CreateStore() => new(NullLogger.Instance, new ModelComparisonStorageOptions { DataDirectory = this.dataDirectory }); + + private static ModelComparisonSeries Series(DateTimeOffset createdAtUtc) => new() + { + Id = Guid.NewGuid(), + Name = "Weekly reports", + CreatedAtUtc = createdAtUtc, + FirstProviderId = "provider-a", + SecondProviderId = "provider-b", + First = new() { LLMProvider = LLMProviders.OPEN_AI, ModelId = "model-a", Label = "Model A" }, + Second = new() { LLMProvider = LLMProviders.MISTRAL, ModelId = "model-b", Label = "Model B" }, + JudgeProviderId = "provider-judge", + Judge = new() { LLMProvider = LLMProviders.ANTHROPIC, ModelId = "judge", Label = "Judge" }, + JudgeInstructions = "I prefer short answers.", + ToolIds = ["web_search"], + DataSourceIds = ["data-source-1"], + RetrievalMode = DataSourceRetrievalMode.SEMANTIC_SEARCH, + Tasks = + [ + new() + { + Id = Guid.NewGuid(), + StartedAtUtc = createdAtUtc, + FinishedAtUtc = createdAtUtc.AddMinutes(20), + QuestionPreview = "Summarize the report.", + FirstModelIsChatOne = false, + ChatOne = new() { ChatId = Guid.NewGuid(), StartedAtUtc = createdAtUtc, FinishedAtUtc = createdAtUtc.AddMinutes(5), UserMessageCount = 2 }, + ChatTwo = new() { ChatId = Guid.NewGuid(), StartedAtUtc = createdAtUtc.AddMinutes(5), FinishedAtUtc = createdAtUtc.AddMinutes(15), UserMessageCount = 3 }, + State = ModelComparisonSeriesTaskState.DONE, + Vote = ModelComparisonModelChoice.SECOND, + JudgeChoice = ModelComparisonModelChoice.FIRST, + JudgeReasoning = "The first chat was more precise.", + }, + ], + }; +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonTallyTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonTallyTests.cs new file mode 100644 index 000000000..5da63ce6d --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonTallyTests.cs @@ -0,0 +1,61 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks how the choices of several runs are counted. +/// +[TestFixture] +public sealed class ModelComparisonTallyTests +{ + [Test] + public void NoRunsCountNothing() + { + var tally = ModelComparisonTally.Of([]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tally.TotalRunCount, Is.Zero); + Assert.That(tally.ScoredRunCount, Is.Zero); + Assert.That(tally.UnscoredRunCount, Is.Zero); + Assert.That(tally.FirstModelWins, Is.Zero); + Assert.That(tally.SecondModelWins, Is.Zero); + Assert.That(tally.Ties, Is.Zero); + } + } + + [Test] + public void EveryChoiceCountsForItsModel() + { + var tally = ModelComparisonTally.Of( + [ + ModelComparisonModelChoice.FIRST, + ModelComparisonModelChoice.SECOND, + ModelComparisonModelChoice.FIRST, + ModelComparisonModelChoice.TIE, + ]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tally.TotalRunCount, Is.EqualTo(4)); + Assert.That(tally.ScoredRunCount, Is.EqualTo(4)); + Assert.That(tally.FirstModelWins, Is.EqualTo(2)); + Assert.That(tally.SecondModelWins, Is.EqualTo(1)); + Assert.That(tally.Ties, Is.EqualTo(1)); + } + } + + [Test] + public void ARunWithoutAChoiceCountsOnlyTowardTheTotal() + { + var tally = ModelComparisonTally.Of([ModelComparisonModelChoice.SECOND, null, null]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(tally.TotalRunCount, Is.EqualTo(3), "Every run happened."); + Assert.That(tally.ScoredRunCount, Is.EqualTo(1), "Only one run had a choice."); + Assert.That(tally.UnscoredRunCount, Is.EqualTo(2)); + Assert.That(tally.SecondModelWins, Is.EqualTo(1)); + } + } +} \ No newline at end of file diff --git a/app/Tests/Assistants/ModelComparison/ModelComparisonWinShareTests.cs b/app/Tests/Assistants/ModelComparison/ModelComparisonWinShareTests.cs new file mode 100644 index 000000000..81f7a1799 --- /dev/null +++ b/app/Tests/Assistants/ModelComparison/ModelComparisonWinShareTests.cs @@ -0,0 +1,71 @@ +using AIStudio.Assistants.ModelComparison; + +namespace AIStudio.Tests.Assistants.ModelComparison; + +/// +/// Checks the share of wins and its Wilson score interval, against values computed by hand. +/// +[TestFixture] +public sealed class ModelComparisonWinShareTests +{ + private const double TOLERANCE = 0.001; + + [Test] + public void WithoutDecidedRunsNothingIsClear() + { + var winShare = ModelComparisonWinShare.Of(0, 0); + + using (Assert.EnterMultipleScope()) + { + Assert.That(winShare.Share, Is.Null); + Assert.That(winShare.LowerBound, Is.Null); + Assert.That(winShare.UpperBound, Is.Null); + Assert.That(winShare.Significance, Is.EqualTo(ModelComparisonSignificance.NOT_YET_CLEAR)); + } + } + + [Test] + public void EightOfTenIsNotYetClear() + { + var winShare = ModelComparisonWinShare.Of(8, 2); + + using (Assert.EnterMultipleScope()) + { + Assert.That(winShare.Share, Is.EqualTo(0.8).Within(TOLERANCE)); + Assert.That(winShare.LowerBound, Is.EqualTo(0.4902).Within(TOLERANCE)); + Assert.That(winShare.UpperBound, Is.EqualTo(0.9433).Within(TOLERANCE)); + Assert.That(winShare.Significance, Is.EqualTo(ModelComparisonSignificance.NOT_YET_CLEAR), "The interval still reaches below one half."); + } + } + + [Test] + public void EighteenOfTwentyIsClearlyBetter() + { + var winShare = ModelComparisonWinShare.Of(18, 2); + + using (Assert.EnterMultipleScope()) + { + Assert.That(winShare.LowerBound, Is.EqualTo(0.6990).Within(TOLERANCE)); + Assert.That(winShare.Significance, Is.EqualTo(ModelComparisonSignificance.CLEARLY_BETTER)); + } + } + + [Test] + public void NoneOfTenIsClearlyWorse() + { + var winShare = ModelComparisonWinShare.Of(0, 10); + + using (Assert.EnterMultipleScope()) + { + Assert.That(winShare.LowerBound, Is.EqualTo(0).Within(TOLERANCE)); + Assert.That(winShare.UpperBound, Is.EqualTo(0.2775).Within(TOLERANCE)); + Assert.That(winShare.Significance, Is.EqualTo(ModelComparisonSignificance.CLEARLY_WORSE)); + } + } + + [Test] + public void ANegativeCountIsRefused() + { + Assert.That(() => ModelComparisonWinShare.Of(-1, 3), Throws.TypeOf()); + } +} \ No newline at end of file diff --git a/app/Tests/Chat/ChatTranscriptTests.cs b/app/Tests/Chat/ChatTranscriptTests.cs new file mode 100644 index 000000000..146f5aa92 --- /dev/null +++ b/app/Tests/Chat/ChatTranscriptTests.cs @@ -0,0 +1,77 @@ +using AIStudio.Chat; +using AIStudio.Tools.ToolCallingSystem; + +namespace AIStudio.Tests.Chat; + +/// +/// Checks that a transcript holds what the user sees of a chat, and nothing else. +/// +[TestFixture] +public sealed class ChatTranscriptTests +{ + [Test] + public void TheTranscriptHoldsTheVisibleMessagesInTheirOrder() + { + var thread = Thread( + Block(ChatRole.USER, new ContentText { Text = "A hidden opening." }, hidden: true), + Block(ChatRole.USER, new ContentText { Text = "Summarize the report." }), + Block(ChatRole.AI, new ContentText { Text = "Let me read it.The report says 42." }), + Block(ChatRole.USER, new ContentText { Text = "Thanks." })); + + Assert.That(ChatTranscript.Of(thread), Is.EqualTo("[User]\nSummarize the report.\n\n[Assistant]\nThe report says 42.\n\n[User]\nThanks.")); + } + + [Test] + public void ToolCallsAreOneShortLineWithoutTheirResults() + { + var answer = new ContentText + { + Text = "Found it.", + ToolInvocations = + [ + new ToolInvocationTrace { Order = 2, ToolName = "read_web_page", Arguments = new() { ["url"] = "https://example.org" }, Result = "The whole page." }, + new ToolInvocationTrace { Order = 1, ToolName = "web_search", Arguments = new() { ["query"] = new string('q', 100) + "\nmore" }, Result = "Many results." }, + ], + }; + + var transcript = ChatTranscript.Of(Thread(Block(ChatRole.AI, answer))); + + using (Assert.EnterMultipleScope()) + { + Assert.That(transcript, Does.StartWith($"[Assistant]\n[Tool call: web_search(query={new string('q', ChatTranscript.MAX_ARGUMENT_LENGTH)}…)]\n[Tool call: read_web_page(url=https://example.org)]\nFound it.")); + Assert.That(transcript, Does.Not.Contain("Many results.")); + Assert.That(transcript, Does.Not.Contain("The whole page.")); + } + } + + [Test] + public void AttachedFilesAreNamedWithoutTheirContent() + { + var message = new ContentText + { + Text = "Please check these.", + FileAttachments = [new FileAttachment(FileAttachmentType.DOCUMENT, "report.pdf", "/tmp/report.pdf", 100), new FileAttachment(FileAttachmentType.DOCUMENT, "notes.md", "/tmp/notes.md", 10)], + }; + + Assert.That(ChatTranscript.Of(Thread(Block(ChatRole.USER, message))), Is.EqualTo("[User]\nPlease check these.\n[Attached files: report.pdf, notes.md]")); + } + + [Test] + public void AChatWithoutVisibleMessagesHasAnEmptyTranscript() + { + var thread = Thread(Block(ChatRole.AI, new ContentText { Text = " " }), Block(ChatRole.USER, new ContentText { Text = "Hidden." }, hidden: true)); + + Assert.That(ChatTranscript.Of(thread), Is.Empty); + } + + private static ChatThread Thread(params ContentBlock[] blocks) => new() { Blocks = [.. blocks] }; + + private static ContentBlock Block(ChatRole role, ContentText content, bool hidden = false) => new() + { + Time = DateTimeOffset.UnixEpoch, + ContentType = ContentType.TEXT, + Role = role, + Content = content, + HideFromUser = hidden, + }; +} \ No newline at end of file diff --git a/app/Tests/Chat/Embedded/EmbeddedChatOpeningTests.cs b/app/Tests/Chat/Embedded/EmbeddedChatOpeningTests.cs new file mode 100644 index 000000000..35fb41418 --- /dev/null +++ b/app/Tests/Chat/Embedded/EmbeddedChatOpeningTests.cs @@ -0,0 +1,71 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; + +namespace AIStudio.Tests.Chat.Embedded; + +/// +/// Checks when an embedded chat sends the message it opens with. +/// +[TestFixture] +public sealed class EmbeddedChatOpeningTests +{ + private static readonly ContentText OPENING_MESSAGE = new() { Text = "What is new today?" }; + + [Test] + public void AnEmptyChatSendsItsOpeningMessage() + { + Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, Conversation(), wasSent: false), Is.True); + } + + [Test] + public void AChatWithoutOpeningMessageSendsNothing() + { + Assert.That(EmbeddedChatOpening.SendsNow(null, Conversation(), wasSent: false), Is.False); + } + + [Test] + public void AChatSendsItsOpeningMessageOnlyOnce() + { + // Until the request adds the message to the chat, the chat still looks empty: + Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, Conversation(), wasSent: true), Is.False); + } + + [Test] + public void AChatInWhichTheUserWroteSendsNothing() + { + var conversation = Conversation(); + conversation.Thread!.Blocks.Add(new ContentBlock { Role = ChatRole.USER, ContentType = ContentType.TEXT, Content = new ContentText { Text = "My own question." } }); + + Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, conversation, wasSent: false), Is.False); + } + + [Test] + public void AChatWhichLoadsOrCannotBeOpenedSendsNothing() + { + var unavailable = Conversation(); + unavailable.Thread = null; + + using (Assert.EnterMultipleScope()) + { + Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, null, wasSent: false), Is.False, "While the chat loads, the message waits."); + Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, unavailable, wasSent: false), Is.False); + } + } + + [Test] + public void AChatInWhichTheModelAnswersSendsNothing() + { + var conversation = Conversation(); + conversation.IsRunning = true; + + Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, conversation, wasSent: false), Is.False); + } + + private static EmbeddedConversation Conversation() => new() + { + ChatId = Guid.NewGuid(), + Component = AIStudio.Tools.Components.MODEL_COMPARISON_ASSISTANT, + Store = new InMemoryEmbeddedChatStore(), + Thread = new ChatThread(), + }; +} \ No newline at end of file diff --git a/app/Tests/Chat/Embedded/EmbeddedChatServiceTests.cs b/app/Tests/Chat/Embedded/EmbeddedChatServiceTests.cs new file mode 100644 index 000000000..b31c21def --- /dev/null +++ b/app/Tests/Chat/Embedded/EmbeddedChatServiceTests.cs @@ -0,0 +1,240 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Tools; +using AIStudio.Tools.AssistantSessions; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.Chat.Embedded; + +/// +/// Checks how EmbeddedChatService runs a request, without asking any provider. +/// +[TestFixture] +public sealed class EmbeddedChatServiceTests +{ + private const AIStudio.Tools.Components COMPONENT = AIStudio.Tools.Components.MODEL_COMPARISON_ASSISTANT; + + private SettingsManager settingsManager = null!; + private ServiceProvider serviceProvider = null!; + private IServiceProvider previousServiceProvider = null!; + private AIStudio.Settings.Provider provider = null!; + + [SetUp] + public void CreateSettings() + { + this.settingsManager = new SettingsManager(NullLogger.Instance, null!); + this.previousServiceProvider = Program.SERVICE_PROVIDER; + this.serviceProvider = new ServiceCollection().AddSingleton(this.settingsManager).BuildServiceProvider(); + Program.SERVICE_PROVIDER = this.serviceProvider; + + this.provider = new AIStudio.Settings.Provider(1, Guid.NewGuid().ToString(), "Instance 1", LLMProviders.DEEP_SEEK, new("test-model", null)); + } + + [TearDown] + public void RestoreApplicationState() + { + Program.SERVICE_PROVIDER = this.previousServiceProvider; + this.serviceProvider.Dispose(); + } + + [Test] + public async Task AChatWithoutAStoredThreadStartsWithANewOne() + { + var service = this.CreateService(Answer("An answer.")); + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var again = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(conversation.IsAvailable, Is.True); + Assert.That(conversation.Thread!.ChatId, Is.EqualTo(chatId), "The new thread gets the ID of the chat."); + Assert.That(again, Is.SameAs(conversation), "A chat is loaded once."); + } + } + + [Test] + public async Task AChatThisVersionCannotOpenTakesNoMessage() + { + var service = this.CreateService(Answer("An answer.")); + var store = new InMemoryEmbeddedChatStore { CannotOpen = true }; + var chatId = Guid.NewGuid(); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var sent = await service.SendAsync(chatId, this.Request("A question.")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(conversation.IsAvailable, Is.False); + Assert.That(sent, Is.False); + Assert.That(store.SaveCount, Is.Zero, "The stored chat stays as it is."); + } + } + + [Test] + public async Task TheMessageIsStoredBeforeTheModelAnswers() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var blocksStoredBeforeTheAnswer = -1; + var service = this.CreateService((_, answer, _, _) => + { + blocksStoredBeforeTheAnswer = store.StoredBlocks(chatId).Count; + answer.Text = "An answer."; + return Task.CompletedTask; + }); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var sent = await service.SendAsync(chatId, this.Request("A question.")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(sent, Is.True); + Assert.That(blocksStoredBeforeTheAnswer, Is.EqualTo(1), "Only the message was stored when the model began."); + Assert.That(store.StoredBlocks(chatId).Select(block => block.Role), Is.EqualTo(new[] { ChatRole.USER, ChatRole.AI })); + Assert.That(conversation.UserMessageCount, Is.EqualTo(1)); + Assert.That(conversation.HasAnswer, Is.True); + Assert.That(conversation.IsRunning, Is.False); + } + } + + [Test] + public async Task AnEmptyAnswerLeavesOnlyTheMessage() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var service = this.CreateService(Answer(string.Empty)); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var sent = await service.SendAsync(chatId, this.Request("A question.")); + + using (Assert.EnterMultipleScope()) + { + Assert.That(sent, Is.True, "The message went in, even without an answer."); + Assert.That(store.StoredBlocks(chatId).Select(block => block.Role), Is.EqualTo(new[] { ChatRole.USER })); + Assert.That(conversation.HasAnswer, Is.False); + } + } + + [Test] + public async Task ARefusedPreparationSendsNothing() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var service = this.CreateService(Answer("An answer.")); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var sent = await service.SendAsync(chatId, this.Request("A question.") with { PrepareAsync = _ => Task.FromResult(false) }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(sent, Is.False); + Assert.That(conversation.Thread!.Blocks, Is.Empty); + Assert.That(store.SaveCount, Is.Zero); + Assert.That(conversation.IsRunning, Is.False, "A refused request does not block the next one."); + } + } + + [Test] + public async Task WithoutAProviderNothingIsSent() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var service = this.CreateService(Answer("An answer.")); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var sent = await service.SendAsync(chatId, this.Request("A question.") with { Provider = AIStudio.Settings.Provider.NONE }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(sent, Is.False); + Assert.That(conversation.Thread!.Blocks, Is.Empty); + } + } + + [Test] + public async Task ASecondMessageWaitsUntilTheFirstIsAnswered() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var answering = new TaskCompletionSource(); + var release = new TaskCompletionSource(); + var service = this.CreateService(async (_, answer, _, _) => + { + answering.SetResult(); + await release.Task; + answer.Text = "An answer."; + }); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var first = service.SendAsync(chatId, this.Request("A question.")); + await answering.Task; + + var second = await service.SendAsync(chatId, this.Request("Another question.")); + release.SetResult(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(second, Is.False); + Assert.That(await first, Is.True); + Assert.That(conversation.UserMessageCount, Is.EqualTo(1)); + } + } + + [Test] + public async Task TheRequestMayCarryMoreThanTheStoredChat() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var blocksSent = -1; + var service = this.CreateService((_, answer, thread, _) => + { + blocksSent = thread.Blocks.Count; + answer.Text = "An answer."; + return Task.CompletedTask; + }); + + await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + var extraBlock = new ContentBlock { Role = ChatRole.USER, ContentType = ContentType.TEXT, Content = new ContentText { Text = "An image description." } }; + await service.SendAsync(chatId, this.Request("A question.") with { ThreadForRequest = thread => thread with { Blocks = [extraBlock, .. thread.Blocks] } }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(blocksSent, Is.EqualTo(3), "The request carried the extra block, the message, and the answer."); + Assert.That(store.StoredBlocks(chatId), Has.Count.EqualTo(2), "The stored chat never holds the extra block."); + } + } + + [Test] + public async Task AHiddenMessageIsNotCountedAsOneOfTheUser() + { + var store = new InMemoryEmbeddedChatStore(); + var chatId = Guid.NewGuid(); + var service = this.CreateService(Answer("Hello, how can I help?")); + + var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread()); + await service.SendAsync(chatId, this.Request("Open the conversation.") with { IsHidden = true }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(conversation.UserMessageCount, Is.Zero); + Assert.That(conversation.HasVisibleMessages, Is.True, "The answer is visible."); + } + } + + private EmbeddedChatService CreateService(Func answerAsync) => new(this.settingsManager, new AssistantSessionService(MessageBus.INSTANCE), MessageBus.INSTANCE, NullLogger.Instance, answerAsync); + + private EmbeddedChatRequest Request(string text) => new() { Provider = this.provider, Message = new ContentText { Text = text } }; + + private static Func Answer(string text) => (_, answer, _, _) => + { + answer.Text = text; + return Task.CompletedTask; + }; +} \ No newline at end of file diff --git a/app/Tests/Chat/Embedded/InMemoryEmbeddedChatStore.cs b/app/Tests/Chat/Embedded/InMemoryEmbeddedChatStore.cs new file mode 100644 index 000000000..c2cd630b2 --- /dev/null +++ b/app/Tests/Chat/Embedded/InMemoryEmbeddedChatStore.cs @@ -0,0 +1,46 @@ +using AIStudio.Chat; +using AIStudio.Chat.Embedded; + +namespace AIStudio.Tests.Chat.Embedded; + +/// +/// Keeps embedded chats in memory, as they were when they were stored last. +/// +public sealed class InMemoryEmbeddedChatStore : IEmbeddedChatStore +{ + private readonly Dictionary threads = []; + + /// + /// Whether loading fails, as with a chat this version cannot open. + /// + public bool CannotOpen { get; init; } + + /// + /// How often a chat was stored. + /// + public int SaveCount { get; private set; } + + /// + /// Gets the blocks of a chat as they were stored last, or none. + /// + /// The ID of the chat. + public IReadOnlyList StoredBlocks(Guid chatId) => this.threads.TryGetValue(chatId, out var thread) ? thread.Blocks : []; + + /// + public Task LoadThreadAsync(Guid chatId, CancellationToken token = default) + { + if (this.CannotOpen) + throw new InvalidDataException("A newer version stored this chat."); + + return Task.FromResult(this.threads.TryGetValue(chatId, out var thread) ? thread : null); + } + + /// + public Task SaveThreadAsync(ChatThread thread, CancellationToken token = default) + { + // A copy of the list, so that what the chat adds later does not change what was stored: + this.threads[thread.ChatId] = thread with { Blocks = [.. thread.Blocks] }; + this.SaveCount++; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/app/Tests/Provider/ProviderIdentityTests.cs b/app/Tests/Provider/ProviderIdentityTests.cs new file mode 100644 index 000000000..5d4eee536 --- /dev/null +++ b/app/Tests/Provider/ProviderIdentityTests.cs @@ -0,0 +1,38 @@ +using AIStudio.Provider; + +namespace AIStudio.Tests.Provider; + +/// +/// Checks how messages to the user name a provider which hides which provider it is, see IProvider.HidesIdentity. +/// +[TestFixture] +public sealed class ProviderIdentityTests +{ + private static readonly Model MODEL = new("gpt-test", null); + + [Test] + public void AProviderNamesItselfByDefault() + { + var provider = new NoProvider { InstanceName = "My provider" }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(provider.NameForUser, Is.EqualTo("My provider")); + Assert.That(provider.TypeForUser, Is.EqualTo(nameof(LLMProviders.NONE))); + Assert.That(provider.ModelNameForUser(MODEL), Is.EqualTo("gpt-test")); + } + } + + [Test] + public void AProviderWhichHidesItsIdentityNamesNothing() + { + var provider = new NoProvider { InstanceName = "My provider", HidesIdentity = true }; + + using (Assert.EnterMultipleScope()) + { + Assert.That(provider.NameForUser, Does.Not.Contain("My provider")); + Assert.That(provider.TypeForUser, Does.Not.Contain(nameof(LLMProviders.NONE))); + Assert.That(provider.ModelNameForUser(MODEL), Does.Not.Contain("gpt-test")); + } + } +} \ No newline at end of file diff --git a/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs b/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs index ad9c4dc30..dc45a391b 100644 --- a/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs +++ b/app/Tests/Serialization/JsonTestRoots.ModelAnswers.cs @@ -2,6 +2,7 @@ using AIStudio.Agents; using AIStudio.Agents.AssistantAudit; using AIStudio.Assistants.Builder; using AIStudio.Assistants.ERI; +using AIStudio.Assistants.ModelComparison; using AIStudio.Assistants.PromptOptimizer; using AIStudio.Assistants.SWOTAnalysis; using AIStudio.Tools.PowerPoint; @@ -28,6 +29,7 @@ public static partial class JsonTestRoots yield return Case(new(typeof(LuaResponse), LuaResponse.JSON_OPTIONS)); yield return Case(new(typeof(LauncherTextsResponse), LauncherTextsResponse.JSON_OPTIONS)); + yield return Case(new(typeof(ModelComparisonJudgeAnswer), ModelComparisonJudgeVerdict.JSON_OPTIONS)); yield return Case(new(typeof(PromptOptimizationResult), AssistantPromptOptimizer.JSON_OPTIONS)); yield return Case(new(typeof(SwotCategoryAnalysisResult), SwotModelAnswer.JSON_OPTIONS)); yield return Case(new(typeof(SwotModerationResult), SwotModelAnswer.JSON_OPTIONS)); diff --git a/app/Tests/Serialization/JsonTestRoots.cs b/app/Tests/Serialization/JsonTestRoots.cs index ca9a434b3..3f3ee1dda 100644 --- a/app/Tests/Serialization/JsonTestRoots.cs +++ b/app/Tests/Serialization/JsonTestRoots.cs @@ -1,6 +1,7 @@ using System.Text.Json; using AIStudio.Assistants.BeyondFactsLab; +using AIStudio.Assistants.ModelComparison; using AIStudio.Assistants.SWOTAnalysis; using AIStudio.Chat; using AIStudio.Settings; @@ -69,6 +70,11 @@ public static partial class JsonTestRoots yield return Case(new(typeof(BflResult), BeyondFactsLabJson.PERSISTENCE)); yield return Case(new(typeof(BflResearch), BeyondFactsLabJson.PERSISTENCE)); yield return Case(new(typeof(Guid?), BeyondFactsLabJson.PERSISTENCE), "selected preparation"); + + // The presets and the stored results of the Model Comparison assistant: + yield return Case(new(typeof(ModelComparisonPreset), ModelComparisonJson.PERSISTENCE)); + yield return Case(new(typeof(ModelComparisonRecord), ModelComparisonJson.PERSISTENCE)); + yield return Case(new(typeof(ModelComparisonSeries), ModelComparisonJson.PERSISTENCE)); } private static TestCaseData Case(JsonTestRoot root, string? purpose = null) diff --git a/app/Tests/Tools/DataBackup/DataBackupWriterTests.cs b/app/Tests/Tools/DataBackup/DataBackupWriterTests.cs index cb3008d0d..d614301f1 100644 --- a/app/Tests/Tools/DataBackup/DataBackupWriterTests.cs +++ b/app/Tests/Tools/DataBackup/DataBackupWriterTests.cs @@ -55,6 +55,7 @@ public sealed class DataBackupWriterTests this.WriteDataFile("tokenizers/openai-gpt-6/tokenizer.json", "{}"); this.WriteDataFile("visualBriefings/briefing/manifest.json", "{}"); this.WriteDataFile("beyondFactsLab/preparation/project.json", "{}"); + this.WriteDataFile("modelComparisons/presets/preset.json", "{}"); this.WriteDataFile("media-staging/operation/transcript.md", "temporary"); this.WriteDataFile("databases/sqlite/rag-index.sqlite3", "the index while AI Studio writes to it"); this.WriteDataFile("databases/provenance/provenance.sqlite3", "the provenance store while AI Studio writes to it"); @@ -93,6 +94,7 @@ public sealed class DataBackupWriterTests "data/tokenizers/openai-gpt-6/tokenizer.json", "data/visualBriefings/briefing/manifest.json", "data/beyondFactsLab/preparation/project.json", + "data/modelComparisons/presets/preset.json", "data/databases/provenance/provenance.sqlite3", "data/databases/sqlite/rag-index.sqlite3", "data/databases/vector_database/stores/store_1/segments/segment", diff --git a/app/Tests/Tools/ProvenanceCleanupServiceTests.cs b/app/Tests/Tools/ProvenanceCleanupServiceTests.cs index 6684256fb..86ef5fb46 100644 --- a/app/Tests/Tools/ProvenanceCleanupServiceTests.cs +++ b/app/Tests/Tools/ProvenanceCleanupServiceTests.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using AIStudio.Assistants.BeyondFactsLab; +using AIStudio.Assistants.ModelComparison; using AIStudio.Chat; using AIStudio.Settings; using AIStudio.Tests.Tools.Databases; @@ -142,6 +143,30 @@ public sealed class ProvenanceCleanupServiceTests Assert.That(entries.Keys, Is.EquivalentTo(new[] { referenced.Value }), "The chat of the lab is read like any other chat."); } + [Test] + public async Task WhatAChatOfAComparisonSeriesReceivedSurvivesTheCleanup() + { + var comparisonStore = this.CreateComparisonStore(); + var seriesId = Guid.NewGuid(); + await comparisonStore.CreateSeriesAsync(new ModelComparisonSeries { Id = seriesId, Name = "Reports" }); + + var thread = new ChatThread { ChatId = Guid.NewGuid() }; + var referenced = await this.store.Client.StoreAsync(thread.ChatId, ProvenanceKind.WEB_PAGE, "of a page the chat of the series read", null, TOKEN); + var unreferenced = await this.store.Client.StoreAsync(thread.ChatId, ProvenanceKind.WEB_PAGE, "of an answer which is gone", null, TOKEN); + thread.Blocks.Add(new ContentBlock + { + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = new ContentText { Text = "According to the page …", SourcePassages = [new SourcePassage { SourceUrl = "https://example.org/", ProvenanceId = referenced }] } + }); + await comparisonStore.SaveSeriesChatAsync(seriesId, thread); + + await this.RunAsync(RestoreStateWith(report: null)); + + var entries = await this.store.Client.GetEntriesAsync([referenced!.Value, unreferenced!.Value], TOKEN); + Assert.That(entries.Keys, Is.EquivalentTo(new[] { referenced.Value }), "The chat of the series is read like any other chat."); + } + [Test] public async Task WhatTheOnboardingOfTheLabReceivedSurvivesTheCleanup() { @@ -181,6 +206,8 @@ public sealed class ProvenanceCleanupServiceTests private BeyondFactsLabStore CreateLabStore() => new(NullLogger.Instance, new BflStorageOptions { DataDirectory = this.testDirectory }); + private ModelComparisonStore CreateComparisonStore() => new(NullLogger.Instance, new ModelComparisonStorageOptions { DataDirectory = this.testDirectory }); + /// /// Stores a chat whose file was written long before the last cleanup, which is noted as if it ran a minute ago. /// @@ -201,7 +228,7 @@ public sealed class ProvenanceCleanupServiceTests // Only entries stored before the start of the service may be removed, and the test stored them just now: await Task.Delay(TimeSpan.FromMilliseconds(10), TOKEN); - using var service = new ProvenanceCleanupService(this.store.CreateClientProvider(), dataRestoreService, this.CreateLabStore(), NullLogger.Instance); + using var service = new ProvenanceCleanupService(this.store.CreateClientProvider(), dataRestoreService, this.CreateLabStore(), this.CreateComparisonStore(), NullLogger.Instance); await service.StartAsync(TOKEN); await service.ExecuteTask!; } diff --git a/app/Tests/Tools/SharedSelectionRulesTests.cs b/app/Tests/Tools/SharedSelectionRulesTests.cs new file mode 100644 index 000000000..02eda0f41 --- /dev/null +++ b/app/Tests/Tools/SharedSelectionRulesTests.cs @@ -0,0 +1,159 @@ +using AIStudio.Provider; +using AIStudio.Settings; +using AIStudio.Settings.DataModel; +using AIStudio.Tools; +using AIStudio.Tools.ToolCallingSystem; + +namespace AIStudio.Tests.Tools; + +/// +/// Checks what a selection of tools or data sources offers when several models work with it. +/// +/// +/// A rule which lets one model have more than another would make a comparison unfair, or send data to a model which +/// may not see it. A rule which gives a single model less than before would change the chat. +/// +[TestFixture] +public sealed class SharedSelectionRulesTests +{ + private static readonly IDataSource ALPHA = DataSource("alpha"); + private static readonly IDataSource BETA = DataSource("beta"); + private static readonly IDataSource GAMMA = DataSource("gamma"); + + [Test] + public void TheLowestConfidenceCounts() + { + Assert.That(SharedSelectionRules.LowestConfidence([ConfidenceLevel.HIGH, ConfidenceLevel.LOW, ConfidenceLevel.MEDIUM]), Is.EqualTo(ConfidenceLevel.LOW)); + } + + [Test] + public void WithoutAModelThereIsNoConfidence() + { + Assert.That(SharedSelectionRules.LowestConfidence([]), Is.EqualTo(ConfidenceLevel.NONE)); + } + + [Test] + public void ToolsNeedEveryModelToCallThem() + { + var unavailable = new ToolCallingAvailability(false, "The second model cannot call tools."); + + Assert.That(SharedSelectionRules.ToolCallingOfAll([ToolCallingAvailability.Available(), unavailable]), Is.EqualTo(unavailable), "The reason of the model which cannot call tools has to reach the user."); + } + + [Test] + public void ToolsAreAvailableWhenEveryModelCallsThem() + { + Assert.That(SharedSelectionRules.ToolCallingOfAll([ToolCallingAvailability.Available(), ToolCallingAvailability.Available()]).IsAvailable, Is.True); + } + + [Test] + public void SemanticSearchNeedsEveryModelToSearchItself() + { + var blocked = new EffectiveRetrievalMode(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.MODEL_CANNOT_USE_TOOLS); + + var mode = SharedSelectionRules.RetrievalModeOfAll([new(DataSourceRetrievalMode.SEMANTIC_SEARCH, ToolOfferBlockReason.NONE), blocked]); + + Assert.That(mode, Is.EqualTo(blocked), "One model searching itself while the other gets passages with every message would make the chats unequal."); + } + + [Test] + public void TheReasonComesAlongWhateverTheUserPrefers() + { + var blocked = new EffectiveRetrievalMode(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.PROVIDER_CONFIDENCE_TOO_LOW); + + var mode = SharedSelectionRules.RetrievalModeOfAll([new(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.NONE), blocked]); + + Assert.That(mode, Is.EqualTo(blocked), "The data source selection leaves out a choice which is none, which it can tell only by the reason."); + } + + [Test] + public void EveryModelSearchingItselfKeepsSemanticSearch() + { + var searchesItself = new EffectiveRetrievalMode(DataSourceRetrievalMode.SEMANTIC_SEARCH, ToolOfferBlockReason.NONE); + + Assert.That(SharedSelectionRules.RetrievalModeOfAll([searchesItself, searchesItself]), Is.EqualTo(searchesItself)); + } + + [Test] + public void ThePreferenceOfTheUserStays() + { + var everyMessage = new EffectiveRetrievalMode(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.NONE); + + Assert.That(SharedSelectionRules.RetrievalModeOfAll([everyMessage, everyMessage]), Is.EqualTo(everyMessage)); + } + + [Test] + public void ASingleModelKeepsItsOwnMode() + { + foreach (var mode in new EffectiveRetrievalMode[] + { + new(DataSourceRetrievalMode.SEMANTIC_SEARCH, ToolOfferBlockReason.NONE), + new(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.NONE), + new(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.TOOLS_SWITCHED_OFF), + }) + Assert.That(SharedSelectionRules.RetrievalModeOfAll([mode]), Is.EqualTo(mode)); + } + + [Test] + public void OnlyTheDataSourcesEveryModelMayUseRemain() + { + var first = new AllowedSelectedDataSources([ALPHA, BETA, GAMMA], [ALPHA, BETA], [], [], []); + var second = new AllowedSelectedDataSources([GAMMA, ALPHA], [ALPHA], [], [], []); + + var shared = SharedSelectionRules.DataSourcesOfAll([first, second]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(shared.AllowedDataSources, Is.EqualTo(new[] { ALPHA, GAMMA }), "The order of the first model counts."); + Assert.That(shared.SelectedDataSources, Is.EqualTo(new[] { ALPHA })); + } + } + + [Test] + public void AWaitingDataSourceStaysWaitingOnlyWhenEveryModelMayUseIt() + { + var first = new AllowedSelectedDataSources([], [], [ALPHA, BETA], [GAMMA], [BETA]); + var second = new AllowedSelectedDataSources([], [], [ALPHA], [GAMMA], []); + + var shared = SharedSelectionRules.DataSourcesOfAll([first, second]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(shared.DataSourcesAwaitingReindex, Is.EqualTo(new[] { ALPHA })); + Assert.That(shared.DataSourcesNeedingRepair, Is.EqualTo(new[] { GAMMA })); + Assert.That(shared.UnreachableDataSources, Is.Empty); + } + } + + [Test] + public void ASingleModelKeepsItsDataSources() + { + var only = new AllowedSelectedDataSources([ALPHA, BETA], [BETA], [GAMMA], [], [ALPHA]); + + var shared = SharedSelectionRules.DataSourcesOfAll([only]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(shared.AllowedDataSources, Is.EqualTo(only.AllowedDataSources)); + Assert.That(shared.SelectedDataSources, Is.EqualTo(only.SelectedDataSources)); + Assert.That(shared.DataSourcesAwaitingReindex, Is.EqualTo(only.DataSourcesAwaitingReindex)); + Assert.That(shared.DataSourcesNeedingRepair, Is.EqualTo(only.DataSourcesNeedingRepair)); + Assert.That(shared.UnreachableDataSources, Is.EqualTo(only.UnreachableDataSources)); + } + } + + [Test] + public void WithoutAModelThereIsNoDataSource() + { + Assert.That(SharedSelectionRules.DataSourcesOfAll([]).AllowedDataSources, Is.Empty); + } + + private static IDataSource DataSource(string id) => new DataSourceLocalFile + { + Num = 1, + Id = id, + Name = id, + Type = DataSourceType.LOCAL_FILE, + ConfidenceLevel = ConfidenceLevel.LOW, + }; +} \ No newline at end of file diff --git a/app/Tests/Tools/ToolCalling/EffectiveRetrievalModeTests.cs b/app/Tests/Tools/ToolCalling/EffectiveRetrievalModeTests.cs index 299a4a526..7a69dd656 100644 --- a/app/Tests/Tools/ToolCalling/EffectiveRetrievalModeTests.cs +++ b/app/Tests/Tools/ToolCalling/EffectiveRetrievalModeTests.cs @@ -84,6 +84,21 @@ public sealed class EffectiveRetrievalModeTests : ToolRegistryTestBase Assert.That(mode, Is.EqualTo(new EffectiveRetrievalMode(DataSourceRetrievalMode.EVERY_MESSAGE, ToolOfferBlockReason.NOT_AVAILABLE_HERE))); } + /// + /// Checks that the chats of a comparison series search their data sources themselves, as every other chat does. + /// + /// + /// Before, the tool was visible in the chat of the app alone, and every comparison series fell back to the + /// classic process without the user having chosen it. + /// + [Test] + public async Task AComparisonSeriesSearchesItself() + { + var mode = await this.GetModeAsync(DataSourceRetrievalMode.SEMANTIC_SEARCH, ToolCapableProvider(), AIStudio.Tools.Components.MODEL_COMPARISON_ASSISTANT); + + Assert.That(mode, Is.EqualTo(new EffectiveRetrievalMode(DataSourceRetrievalMode.SEMANTIC_SEARCH, ToolOfferBlockReason.NONE))); + } + // Stating its definition needs none of the services the tool searches with: private Task GetModeAsync(DataSourceRetrievalMode preference, AIStudio.Settings.Provider provider, AIStudio.Tools.Components component = AIStudio.Tools.Components.CHAT) { diff --git a/app/Tests/Tools/ToolCalling/SemanticSearchToolDefinitionTests.cs b/app/Tests/Tools/ToolCalling/SemanticSearchToolDefinitionTests.cs index f865f3724..15d11b813 100644 --- a/app/Tests/Tools/ToolCalling/SemanticSearchToolDefinitionTests.cs +++ b/app/Tests/Tools/ToolCalling/SemanticSearchToolDefinitionTests.cs @@ -13,7 +13,8 @@ namespace AIStudio.Tests.Tools.ToolCalling; /// The registry drops a definition it cannot accept with no more than a warning in the log. For /// Semantic Search, that would quietly bring back the classic RAG process in every chat. The tool /// is offered without anybody selecting it, and only where there are data sources to search: in a -/// chat, never in an assistant, and only when the user lets the model search them itself. +/// chat, including the chats of a comparison series, never in another assistant, and only when the +/// user lets the model search them itself. /// [TestFixture] [NonParallelizable] @@ -47,6 +48,52 @@ public sealed class SemanticSearchToolDefinitionTests : ToolRegistryTestBase Assert.That(runnableTools, Is.Empty, "An assistant has no data sources to search, even when something names the tool."); } + [Test] + public async Task AComparisonSeriesIsOfferedTheToolWithoutSelectingIt() + { + var registry = this.CreateRegistry(new TestTool(Tool().GetDefinition())); + var provider = ToolCapableProvider(); + var context = new ToolResolutionContext + { + Provider = provider, + Component = AIStudio.Tools.Components.MODEL_COMPARISON_ASSISTANT, + ProviderConfidence = provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level, + ChatThread = new ChatThread(), + }; + + var runnableTools = await registry.GetRunnableToolsAsync(context, [], mayRunTools: true); + + Assert.That(runnableTools.Select(tool => tool.Definition.Id), Is.EqualTo(new[] { ToolSelectionRules.SEMANTIC_SEARCH_TOOL_ID }), "The chats of a comparison series are chats, and the user chose their data sources."); + } + + /// + /// Checks that the Answer Test of the Model Comparison assistant offers no data source, though it runs as the same + /// component as the comparison series. + /// + /// + /// Its chats keep the data source options of a new chat thread, which switch the data sources off. The preview of + /// the data sources is on, so that the test does not pass because of it. + /// + [Test] + public async Task TheAnswerTestOffersNoDataSource() + { + this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Add(PreviewFeatures.PRE_RAG_2024); + var provider = ToolCapableProvider(); + var context = new ToolResolutionContext + { + Provider = provider, + Component = AIStudio.Tools.Components.MODEL_COMPARISON_ASSISTANT, + ProviderConfidence = provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level, + ChatThread = new ChatThread(), + }; + + // Switched-off data sources decide before any data source is checked, so the tool needs no other of its services: + var tool = new SemanticSearchTool(this.SettingsManager, null!, null!, null!, null!); + var function = await tool.ResolveFunctionAsync(tool.GetDefinition(), context); + + Assert.That(function, Is.Null, "Both models of the Answer Test receive the request alone."); + } + [Test] public async Task AChatWhichSearchesWithEveryMessageIsNotOfferedTheTool() { diff --git a/documentation/Tools.md b/documentation/Tools.md index daa064050..907208518 100644 --- a/documentation/Tools.md +++ b/documentation/Tools.md @@ -224,6 +224,8 @@ A call has to pass both the restriction and the free address choice, so with the `semantic_search` lets the model search the data sources of a chat itself, with a query it works out from the conversation, whenever a question calls for it. The classic RAG process, `AISrcSelWithRetCtxVal`, searches them with every message instead, using the message as the query. One place decides which of the two runs, `ToolRegistry.GetEffectiveRetrievalModeAsync`. Semantic search is the default, and the user can choose the other way per chat through `DataSourceOptions.RetrievalMode`. Whenever the tool cannot be offered — a model without tool calling, tools or this tool switched off, a provider below a confidence the organization set for it — the classic process searches instead. That process steps back only when the answer is semantic search, so a chat never ends up searching nothing. +Chats run in two places: in the chat of the app, and in the comparison series of the Model Comparison assistant, whose chats run as `Components.MODEL_COMPARISON_ASSISTANT`. `semantic_search` is visible in both, see `ToolVisibilityDefinition.AllowedComponents`; anywhere else, `GetEffectiveRetrievalModeAsync` answers `NOT_AVAILABLE_HERE`, and the data sources are searched with every message. The mode Answer Variance of the Model Comparison assistant runs as the same component, but its chats keep the data sources switched off, so the tool offers nothing there. + The tool offers the data sources of the chat which the provider may use. With the automatic selection switched on, those are all data sources the provider may use: the AI which selects is the chat model itself. No agent takes part, so `DataSourceService` counts only the chat provider when it decides what may be used, see `DataSourceRetrievalMode`. The description of the tool lists the offered data sources by ID, name, and kind, with their own description shortened to 500 characters. It names no page size: how many passages a page holds depends on how long they are, so each result tells whether a further page holds more. The description of an ERI data source comes from its server, so it goes through the prompt-injection filter before it gets there. The listed IDs are the only values `data_source_ids` accepts. An ERI data source has a single page of matches, see `IDataSource.GetLastPage`, and asks its server for exactly as many as the classic process does. ERI v1 knows no offset, so a further page would have to ask for all pages before it as well, and a server which allows only the configured number of matches would refuse it: the model would hear that the data source could not be searched rather than that there is nothing more. When these matches do not fit into one search, the model still turns pages within them, and each page asks the server again. diff --git a/runtime/src/data_restore.rs b/runtime/src/data_restore.rs index 393053e23..3500cc5a6 100644 --- a/runtime/src/data_restore.rs +++ b/runtime/src/data_restore.rs @@ -52,7 +52,7 @@ const SETTINGS_FILE_NAME: &str = "settings.json"; /// (`DataBackupContents`) includes. The runtime refuses a plan which names anything else, so that /// whatever goes wrong in the app, a restore never replaces the logs, the downloaded tools, or the /// data folder as a whole. -const REPLACEABLE_ELEMENTS: [(Location, &str); 11] = [ +const REPLACEABLE_ELEMENTS: [(Location, &str); 12] = [ (Location::ConfigDirectory, SETTINGS_FILE_NAME), (Location::DataDirectory, "workspaces/"), (Location::DataDirectory, "tempChats/"), @@ -61,6 +61,7 @@ const REPLACEABLE_ELEMENTS: [(Location, &str); 11] = [ (Location::DataDirectory, "tokenizers/"), (Location::DataDirectory, "visualBriefings/"), (Location::DataDirectory, "beyondFactsLab/"), + (Location::DataDirectory, "modelComparisons/"), (Location::DataDirectory, "databases/sqlite/"), (Location::DataDirectory, "databases/vector_database/stores/"), (Location::DataDirectory, "mediaTranscripts/"), @@ -827,6 +828,21 @@ mod tests { assert!(!test.exists(Location::DataDirectory, "beyondFactsLab/current")); } + #[test] + fn a_restore_replaces_the_files_of_the_model_comparison() { + let test = TestFolders::new(); + test.write_current_data(); + test.write(Location::DataDirectory, "modelComparisons/presets/current.json", "current preset"); + test.prepare_restore(r#"{ "location": "DATA_DIRECTORY", "relative_path": "modelComparisons/", "keep": [] }"#); + test.write(Location::DataDirectory, ".restore-pending/modelComparisons/presets/restored.json", "restored preset"); + + let outcome = apply_pending_restore_in(&test.folders(), &[]); + + assert_eq!(outcome, Some(RestoreOutcome::Applied)); + assert_eq!(test.read(Location::DataDirectory, "modelComparisons/presets/restored.json").as_deref(), Some("restored preset")); + assert!(!test.exists(Location::DataDirectory, "modelComparisons/presets/current.json")); + } + #[test] fn a_plan_which_names_another_element_is_refused() { for relative_path in ["", "events.log", "databases/", "../data/", "pandoc/"] {