diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index 22c71381..6da79ad5 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Diagnostics.CodeAnalysis; using AIStudio.Chat; using AIStudio.Dialogs; @@ -371,11 +370,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore(provider.InstanceName, provider.Id)); } @@ -459,7 +457,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore x.Id == this.selectedPolicy.PreselectedProvider); - if (policyProvider is not null && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) + var policyProvider = this.SettingsManager.GetProviderById(this.selectedPolicy.PreselectedProvider); + if (policyProvider != Settings.Provider.NONE && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) { this.ProviderSettings = policyProvider; this.CurrentProfile = this.ResolveProfileSelection(); diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs index 8666bc12..41ead1f6 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEditorState.cs @@ -1,9 +1,8 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Assistants.SlideBuilder; using AIStudio.Chat; using AIStudio.Settings; +using ComponentKind = AIStudio.Tools.Components; using ProviderSettings = AIStudio.Settings.Provider; namespace AIStudio.Assistants.VisualBriefing; @@ -77,7 +76,6 @@ public sealed class VisualBriefingEditorState /// The manifest to read. /// The settings used to resolve the stored provider and profile. /// The editor state for the briefing. - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "A stored briefing references one specific provider and model by id, so it must be looked up directly instead of using the preselection APIs.")] public static VisualBriefingEditorState FromManifest(VisualBriefingManifest briefing, SettingsManager settingsManager) => new() { Name = briefing.Name, @@ -94,8 +92,8 @@ public sealed class VisualBriefingEditorState ProtectionLevel = briefing.Settings.ProtectionLevel, CustomProtectionLevel = briefing.Settings.CustomProtectionLevel, - Provider = settingsManager.ConfigurationData.Providers.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProviderId && candidate.Model.Id == briefing.Settings.ModelId) ?? ProviderSettings.NONE, - Profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(candidate => candidate.Id == briefing.Settings.ProfileId) ?? Profile.NO_PROFILE, + Provider = ResolveProvider(briefing, settingsManager), + Profile = settingsManager.GetProfileById(briefing.Settings.ProfileId), SourceMaterial = [ @@ -112,6 +110,43 @@ public sealed class VisualBriefingEditorState ], }; + /// + /// Resolves the provider a stored briefing refers to. + /// + /// + /// + /// A briefing stores its provider and model as two separate ids, and both must still match: when + /// the user changed the model of that provider, the stored combination no longer exists and the + /// editor starts without a provider. + /// + /// + /// The resolved provider is additionally checked against the minimum confidence level of the + /// visual briefing assistant. This matters because the confidence settings may have become + /// stricter since the briefing was stored: the user may have lowered the confidence of that + /// provider, or may now enforce a global minimum. Without this check, opening an old briefing + /// would silently restore a provider the user no longer trusts, bypassing the filtering that + /// the provider dropdown applies. Note that the component minimum already covers the enforced + /// global minimum as well. + /// + /// + /// The manifest to read. + /// The settings used to resolve the provider. + /// The stored provider, or when it is unavailable or no longer trusted. + private static ProviderSettings ResolveProvider(VisualBriefingManifest briefing, SettingsManager settingsManager) + { + var storedProvider = settingsManager.GetProviderById(briefing.Settings.ProviderId); + if (storedProvider == ProviderSettings.NONE) + return ProviderSettings.NONE; + + if (storedProvider.Model.Id != briefing.Settings.ModelId) + return ProviderSettings.NONE; + + if (!settingsManager.IsProviderConfident(storedProvider, ComponentKind.VISUAL_BRIEFING_ASSISTANT)) + return ProviderSettings.NONE; + + return storedProvider; + } + /// /// Creates the persisted settings for this editor state. /// diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index 8267219c..722ad15f 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; @@ -35,27 +33,20 @@ public partial class ConfigurationProviderSelection : MSGComponentBase [Parameter] public Func IsLocked { get; set; } = () => false; - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private IEnumerable> FilteredData() { if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS) yield return new(T("Use app default"), string.Empty); - - // Get the minimum confidence level for this component, and/or the enforced global minimum confidence level: - var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component); - - // Apply the explicit minimum confidence level if set and higher than the current minimum level: - if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel) - minimumLevel = this.ExplicitMinimumConfidence; - - // Filter the providers based on the minimum confidence level: + + // + // Filter the providers based on the minimum confidence level of this component, the enforced + // global minimum, and the explicit minimum level when it is higher. Providers which no longer + // exist resolve to `Provider.NONE` and are dropped by the confidence check as well: + // foreach (var providerId in this.Data) { - var provider = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == providerId.Value); - if (provider is null) - continue; - - if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) + var provider = this.SettingsManager.GetProviderById(providerId.Value); + if (this.SettingsManager.IsProviderConfident(provider, this.Component, this.ExplicitMinimumConfidence)) yield return providerId; } } diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs index de7b668c..20313116 100644 --- a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Provider; using AIStudio.Settings; @@ -83,7 +81,6 @@ public partial class ProviderSelection : MSGComponentBase _ => this.T("Uses reasoning (thinking)"), }; - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private IEnumerable GetAvailableProviders() { switch (this.Component) @@ -91,25 +88,17 @@ public partial class ProviderSelection : MSGComponentBase case null: this.Logger.LogError("Component is null! Cannot filter providers based on component settings. Missed CascadingParameter?"); yield break; - + case Tools.Components.NONE: this.Logger.LogError("Component is NONE! Cannot filter providers based on component settings. Used wrong component?"); yield break; - + case { } component: - - // Get the minimum confidence level for this component, and/or the global minimum if enforced: - var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(component); - - // Override with the explicit minimum level if set and higher: - if (this.ExplicitMinimumConfidence is not ConfidenceLevel.UNKNOWN && this.ExplicitMinimumConfidence > minimumLevel) - minimumLevel = this.ExplicitMinimumConfidence; - - // Filter providers based on the minimum confidence level: - foreach (var provider in this.SettingsManager.ConfigurationData.Providers) - if (provider.UsedLLMProvider != LLMProviders.NONE) - if (provider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel) - yield return provider; + + // Filter providers based on the minimum confidence level of this component, the + // enforced global minimum, and the explicit minimum level when it is higher: + foreach (var provider in this.SettingsManager.GetConfidentProviders(component, this.ExplicitMinimumConfidence)) + yield return provider; break; } } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 5ec93e3e..92030f3f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -9,7 +9,7 @@ @T("What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. You can configure as many providers as you want. This way, you can use the appropriate model for each task. As an LLM provider, you can also choose local providers. However, to use this app, you must configure at least one provider.") - + @@ -66,7 +66,7 @@ - @if(this.SettingsManager.ConfigurationData.Providers.Count == 0) + @if(this.SettingsManager.GetAllProviders().Count == 0) { @T("No providers configured yet.") diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index 4e86eed9..9e073563 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -27,7 +27,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase #endregion - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")] private async Task AddLLMProvider() { var dialogParameters = new DialogParameters @@ -50,7 +50,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")] private async Task EditLLMProvider(AIStudio.Settings.Provider provider) { if(provider == AIStudio.Settings.Provider.NONE) @@ -94,7 +94,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] + [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")] private async Task DeleteLLMProvider(AIStudio.Settings.Provider provider) { var dialogParameters = new DialogParameters @@ -156,11 +156,10 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName; } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private async Task UpdateProviders() { this.AvailableLLMProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.Providers) + foreach (var provider in this.SettingsManager.GetAllProviders()) this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id)); await this.AvailableLLMProvidersChanged.InvokeAsync(this.AvailableLLMProviders); diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index efa32f91..bd88ba21 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -201,9 +201,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); // Load the used instance names: - #pragma warning disable MWAIS0001 - this.UsedInstanceNames = this.SettingsManager.ConfigurationData.Providers.Select(x => x.InstanceName.ToLowerInvariant()).ToList(); - #pragma warning restore MWAIS0001 + this.UsedInstanceNames = this.SettingsManager.GetAllProviders().Select(x => x.InstanceName.ToLowerInvariant()).ToList(); this.capabilityOverrides = this.DataCapabilityOverrides ?? new(); this.showExpertSettings = !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters) || this.capabilityOverrides.HasOverrides; diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs index bb214e1f..d93a0263 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using AIStudio.Components; using AIStudio.Settings; using AIStudio.Tools.Services; @@ -40,11 +38,10 @@ public abstract class SettingsDialogBase : MSGComponentBase protected void Close() => this.MudDialog.Cancel(); - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private void UpdateProviders() { this.AvailableLLMProviders.Clear(); - foreach (var provider in this.SettingsManager.ConfigurationData.Providers) + foreach (var provider in this.SettingsManager.GetAllProviders()) this.AvailableLLMProviders.Add(new (provider.InstanceName, provider.Id)); } diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 09081ff0..8eb924bb 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Text.Json; @@ -434,7 +433,6 @@ public sealed class SettingsManager return localeTag[..separatorIndex]; } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false) { var minimumLevel = this.GetMinimumConfidenceLevel(component); @@ -486,15 +484,27 @@ public sealed class SettingsManager return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider && x.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) ?? Provider.NONE; } - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] public Provider GetChatProviderForLoadedChat(string? chatProviderId = null) { var minimumLevel = this.GetMinimumConfidenceLevel(Tools.Components.CHAT); - bool IsSelectableProvider(Provider provider) => - provider != Provider.NONE - && provider.UsedLLMProvider != LLMProviders.NONE - && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel; + var chatProvider = FindProviderById(chatProviderId); + if (chatProvider is not null) + return chatProvider; + + var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions + ? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider) + : null; + + if (defaultChatProvider is not null) + return defaultChatProvider; + + var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider); + if (defaultAppProvider is not null) + return defaultAppProvider; + + var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList(); + return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE; Provider? FindProviderById(string? providerId) { @@ -505,22 +515,103 @@ public sealed class SettingsManager return provider is not null && IsSelectableProvider(provider) ? provider : null; } - var chatProvider = FindProviderById(chatProviderId); - if (chatProvider is not null) - return chatProvider; + bool IsSelectableProvider(Provider provider) => + provider != Provider.NONE + && provider.UsedLLMProvider != LLMProviders.NONE + && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel; + } - var defaultChatProvider = this.ConfigurationData.Chat.PreselectOptions - ? FindProviderById(this.ConfigurationData.Chat.PreselectedProvider) - : null; - if (defaultChatProvider is not null) - return defaultChatProvider; + /// + /// Returns all configured providers without applying any confidence filtering. + /// + /// + /// + /// This method applies neither the global minimum confidence level (see + /// with EnforceGlobalMinimumConfidence) nor any + /// component-specific minimum. Even when the user enforces a global minimum of, say, + /// , this method still returns every configured provider. + /// That is intentional: this method serves the provider management UI, duplicate-name checks, + /// and the raw select data of provider dropdowns. The dropdowns are filtered afterward by + /// ConfigurationProviderSelection, which calls IsProviderConfident. + /// + /// + /// Whenever a provider is about to be used for an LLM request, do not use this method. Use + /// GetConfidentProviders, GetPreselectedProvider, or GetChatProviderForLoadedChat instead, + /// since they honor the confidence levels. + /// + /// + /// The returned list is the live provider list. It is read-only for callers: adding, editing, + /// or removing providers stays inside the settings UI. + /// + /// + /// All configured providers, unfiltered. + public IReadOnlyList GetAllProviders() => this.ConfigurationData.Providers; - var defaultAppProvider = FindProviderById(this.ConfigurationData.App.PreselectedProvider); - if (defaultAppProvider is not null) - return defaultAppProvider; + /// + /// Returns the provider with the given id, without applying any confidence filtering. + /// + /// + /// This method resolves a stored provider reference by its id. It applies neither the global + /// minimum confidence level nor any component-specific minimum, so it returns the requested + /// provider even when the user enforces a higher global minimum. Callers that intend to use the + /// returned provider for an LLM request must check it themselves through + /// IsProviderConfident or fall back to GetPreselectedProvider. + /// + /// The id of the provider to look up. + /// The provider, or when no provider with that id exists. + public Provider GetProviderById(string? providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + return Provider.NONE; - var selectableProviders = this.ConfigurationData.Providers.Where(IsSelectableProvider).ToList(); - return selectableProviders.Count == 1 ? selectableProviders[0] : Provider.NONE; + if (string.Equals(providerId, Provider.NONE.Id, StringComparison.OrdinalIgnoreCase)) + return Provider.NONE; + + return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id.Equals(providerId, StringComparison.OrdinalIgnoreCase)) ?? Provider.NONE; + } + + /// + /// Determines the minimum confidence level a provider must have for the given component. + /// + /// The component for which the providers get filtered. + /// An explicit minimum level, which is applied when it is higher than the component's minimum. + /// The effective minimum confidence level. + public ConfidenceLevel GetEffectiveMinimumConfidenceLevel(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) + { + var minimumLevel = this.GetMinimumConfidenceLevel(component); + if (explicitMinimum is not ConfidenceLevel.UNKNOWN && explicitMinimum > minimumLevel) + return explicitMinimum; + + return minimumLevel; + } + + /// + /// Checks whether the given provider satisfies the minimum confidence level of the given component. + /// + /// The provider to check. + /// The component for which the provider gets checked. + /// An explicit minimum level, which is applied when it is higher than the component's minimum. + /// True, when the provider may be used by the component, false otherwise. + public bool IsProviderConfident(Provider provider, Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) + { + if (provider.UsedLLMProvider is LLMProviders.NONE) + return false; + + return provider.UsedLLMProvider.GetConfidence(this).Level >= this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum); + } + + /// + /// Returns all providers that satisfy the minimum confidence level of the given component. + /// + /// The component for which the providers get filtered. + /// An explicit minimum level, which is applied when it is higher than the component's minimum. + /// All providers the component may use. + public IEnumerable GetConfidentProviders(Tools.Components component, ConfidenceLevel explicitMinimum = ConfidenceLevel.UNKNOWN) + { + var minimumLevel = this.GetEffectiveMinimumConfidenceLevel(component, explicitMinimum); + foreach (var provider in this.ConfigurationData.Providers) + if (provider.UsedLLMProvider is not LLMProviders.NONE && provider.UsedLLMProvider.GetConfidence(this).Level >= minimumLevel) + yield return provider; } public Profile GetPreselectedProfile(Tools.Components component) diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index 1dc1e5c9..a47823ec 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Settings.DataModel; @@ -164,48 +163,42 @@ public static class ComponentsExtensions _ => default, }; - [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] - public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) + public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch { - var preselectedProvider = component switch - { - Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : null, - Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : null, - Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : null, - Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : null, - Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Translation.PreselectedProvider) : null, - Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Agenda.PreselectedProvider) : null, - Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Coding.PreselectedProvider) : null, - Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : null, - Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.EMail.PreselectedProvider) : null, - Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : null, - Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : null, - Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : null, - Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : null, - Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : null, - Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : null, - Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.I18N.PreselectedProvider) : null, - Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : null, - Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), - - // The Document Analysis Assistant does not have a preselected provider at the component level. - // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. - Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, + Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE, + Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE, + Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE, + Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE, + Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE, + Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE, + Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE, + Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE, + Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE, + Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE, + Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE, + Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE, + Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE, + Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE, + Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE, + Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE, + Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), - Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : null, + // The Document Analysis Assistant does not have a preselected provider at the component level. + // The provider is selected per policy instead. We do this inside the Document Analysis Assistant component. + Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE, - Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : null, + Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE, - Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : null, - Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : null, - Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : null, - Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE, - _ => Settings.Provider.NONE, - }; - - return preselectedProvider ?? Settings.Provider.NONE; - } + Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE, + Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider), + + _ => Settings.Provider.NONE, + }; public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) { diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs index a2db69df..2c57bd33 100644 --- a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs +++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/ProviderAccessAnalyzer.cs @@ -17,11 +17,16 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer private static readonly string TITLE = "Direct access to `Providers` is not allowed"; - private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetPreselectedProvider`, etc."; + private static readonly string MESSAGE_FORMAT = "Direct access to `SettingsManager.ConfigurationData.Providers` is not allowed. Instead, use APIs like `SettingsManager.GetAllProviders`, `GetProviderById`, `GetConfidentProviders`, `GetPreselectedProvider`, or `GetChatProviderForLoadedChat`."; private static readonly string DESCRIPTION = MESSAGE_FORMAT; private const string CATEGORY = "Usage"; + + /// + /// The one type which owns the provider list and is therefore allowed to access it directly. + /// + private const string OWNING_TYPE = "AIStudio.Settings.SettingsManager"; private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION); @@ -29,7 +34,12 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + // + // We analyze generated code as well, because Razor markup ends up in generated files. Without + // this, any `ConfigurationData.Providers` access written directly in a `.razor` file would + // bypass this rule entirely. The Razor compiler maps the diagnostic back to the `.razor` line: + // + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(this.AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); } @@ -42,8 +52,17 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer if (memberAccess.Name.Identifier.Text != "Providers") return; + // + // The settings manager owns the provider list: it implements the very APIs which all other + // code is meant to use, so it must access `Providers` directly. Exempting it here keeps + // those implementations free of suppression attributes, which would otherwise read as if + // suppressing this rule was a normal thing to do: + // + if (IsOwningType(context.ContainingSymbol)) + return; + // Get the full path of the member access: - var fullPath = this.GetFullMemberAccessPath(memberAccess); + var fullPath = GetFullMemberAccessPath(memberAccess); // Check for the forbidden pattern: if (fullPath.EndsWith("ConfigurationData.Providers")) @@ -53,7 +72,30 @@ public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer } } - private string GetFullMemberAccessPath(ExpressionSyntax expression) + /// + /// Checks whether the analyzed node sits inside the type which owns the provider list. + /// + /// + /// The containing symbol is the member the node belongs to, e.g. a method or a property. We walk + /// the chain of containing types so that nested types of the owning type are covered as well. + /// + /// The symbol containing the analyzed node, which may be null. + /// True, when the node belongs to the owning type. + private static bool IsOwningType(ISymbol? containingSymbol) + { + var containingType = containingSymbol as INamedTypeSymbol ?? containingSymbol?.ContainingType; + while (containingType != null) + { + if (containingType.ToDisplayString() == OWNING_TYPE) + return true; + + containingType = containingType.ContainingType; + } + + return false; + } + + private static string GetFullMemberAccessPath(ExpressionSyntax expression) { var parts = new List(); while (expression is MemberAccessExpressionSyntax memberAccess)