2025-02-23 14:05:29 +00:00
using System.Diagnostics.CodeAnalysis ;
2025-06-27 20:52:34 +00:00
using System.Linq.Expressions ;
2024-04-19 19:19:13 +00:00
using System.Text.Json ;
2024-07-28 19:18:17 +00:00
2024-09-11 21:08:02 +00:00
using AIStudio.Provider ;
2024-07-28 09:20:00 +00:00
using AIStudio.Settings.DataModel ;
2025-03-29 17:40:17 +00:00
using AIStudio.Tools.PluginSystem ;
2025-04-12 19:13:33 +00:00
using AIStudio.Tools.Services ;
2024-04-19 19:19:13 +00:00
2024-04-20 15:06:50 +00:00
// ReSharper disable NotAccessedPositionalProperty.Local
2024-04-19 19:19:13 +00:00
namespace AIStudio.Settings ;
2024-05-04 08:55:00 +00:00
/// <summary>
/// The settings manager.
/// </summary>
2025-05-03 19:47:43 +00:00
public sealed class SettingsManager
2024-04-19 19:19:13 +00:00
{
private const string SETTINGS_FILENAME = "settings.json" ;
2026-06-21 16:46:21 +00:00
private const Version CURRENT_SETTINGS_VERSION = Version . V6 ;
private readonly record struct SettingsVersionReadResult ( Version Version , SettingsWriteBlockReason FailureReason ) ;
private readonly record struct CurrentSettingsReadResult ( Data ? SettingsData , SettingsWriteBlockReason FailureReason ) ;
2024-04-19 19:19:13 +00:00
2024-07-28 19:18:17 +00:00
private static readonly JsonSerializerOptions JSON_OPTIONS = new ( )
{
WriteIndented = true ,
2025-04-12 08:08:36 +00:00
Converters = { new TolerantEnumConverter ( ) } ,
2024-07-28 19:18:17 +00:00
} ;
2024-09-01 18:10:03 +00:00
2025-05-03 19:47:43 +00:00
private readonly ILogger < SettingsManager > logger ;
private readonly RustService rustService ;
/// <summary>
/// The settings manager.
/// </summary>
public SettingsManager ( ILogger < SettingsManager > logger , RustService rustService )
{
this . logger = logger ;
this . rustService = rustService ;
this . logger . LogInformation ( "Settings manager created." ) ;
}
2024-05-04 08:55:00 +00:00
/// <summary>
/// The directory where the configuration files are stored.
/// </summary>
2024-04-19 19:19:13 +00:00
public static string? ConfigDirectory { get ; set ; }
2024-05-04 08:55:00 +00:00
/// <summary>
/// The directory where the data files are stored.
/// </summary>
2024-04-19 19:19:13 +00:00
public static string? DataDirectory { get ; set ; }
2024-09-15 10:30:07 +00:00
/// <summary>
/// Whether the app is in dark mode.
/// </summary>
public bool IsDarkMode { get ; set ; }
2026-03-21 17:05:06 +00:00
/// <summary>
/// Ensures that the startup start-page redirect is evaluated at most once per app session.
/// </summary>
public bool StartupStartPageRedirectHandled { get ; set ; }
/// <summary>
/// Indicates that the initial settings load attempt has completed.
/// </summary>
public bool HasCompletedInitialSettingsLoad { get ; private set ; }
2026-06-21 16:46:21 +00:00
/// <summary>
/// Indicates why settings writes are blocked for the current session.
/// </summary>
public SettingsWriteBlockReason SettingsWriteBlockReason { get ; private set ; } = SettingsWriteBlockReason . NONE ;
/// <summary>
/// Indicates that settings writes are blocked for the current session.
/// </summary>
public bool SettingsWriteBlocked = > this . SettingsWriteBlockReason is not SettingsWriteBlockReason . NONE ;
2024-05-04 08:55:00 +00:00
/// <summary>
/// The configuration data.
/// </summary>
2024-04-20 15:06:50 +00:00
public Data ConfigurationData { get ; private set ; } = new ( ) ;
private bool IsSetUp = > ! string . IsNullOrWhiteSpace ( ConfigDirectory ) & & ! string . IsNullOrWhiteSpace ( DataDirectory ) ;
2024-05-04 08:55:00 +00:00
/// <summary>
/// Loads the settings from the file system.
/// </summary>
2024-04-20 15:06:50 +00:00
public async Task LoadSettings ( )
2026-03-10 19:50:45 +00:00
{
var settingsSnapshot = await this . TryReadSettingsSnapshot ( ) ;
if ( settingsSnapshot is not null )
this . ConfigurationData = settingsSnapshot ;
2026-03-21 17:05:06 +00:00
this . HasCompletedInitialSettingsLoad = true ;
2026-03-10 19:50:45 +00:00
}
/// <summary>
/// Reads the settings from disk without mutating the current in-memory state.
/// </summary>
/// <returns>A (migrated) settings snapshot, or null if it could not be read.</returns>
public async Task < Data ? > TryReadSettingsSnapshot ( )
2024-04-19 19:19:13 +00:00
{
2026-06-21 16:46:21 +00:00
this . SettingsWriteBlockReason = SettingsWriteBlockReason . NONE ;
2024-04-19 19:19:13 +00:00
if ( ! this . IsSetUp )
2024-09-01 18:10:03 +00:00
{
this . logger . LogWarning ( "Cannot load settings, because the configuration is not set up yet." ) ;
2026-03-10 19:50:45 +00:00
return null ;
2024-09-01 18:10:03 +00:00
}
2024-04-19 19:19:13 +00:00
var settingsPath = Path . Combine ( ConfigDirectory ! , SETTINGS_FILENAME ) ;
if ( ! File . Exists ( settingsPath ) )
2024-09-01 18:10:03 +00:00
{
this . logger . LogWarning ( "Cannot load settings, because the settings file does not exist." ) ;
2026-03-10 19:50:45 +00:00
return null ;
2024-09-01 18:10:03 +00:00
}
2026-06-21 16:46:21 +00:00
var settingsVersion = await this . TryReadSettingsVersion ( settingsPath ) ;
if ( settingsVersion . FailureReason is not SettingsWriteBlockReason . NONE )
2024-08-05 19:12:52 +00:00
{
2026-06-21 16:46:21 +00:00
this . BlockSettingsWrites ( settingsVersion . FailureReason , "The settings file version could not be identified. Settings writes are blocked to avoid overwriting newer or unreadable settings." ) ;
return await this . TryReadCurrentVersionBackupSnapshotForBlockedSettings ( ) ;
}
2024-08-05 19:12:52 +00:00
2026-06-21 16:46:21 +00:00
if ( settingsVersion . Version > CURRENT_SETTINGS_VERSION )
{
this . BlockSettingsWrites ( SettingsWriteBlockReason . VERSION_NEWER_THAN_APP , $"The settings file uses the newer version '{settingsVersion.Version}'. Settings writes are blocked to avoid overwriting newer settings." ) ;
return await this . TryReadCurrentVersionBackupSnapshotForBlockedSettings ( ) ;
}
2026-03-10 19:50:45 +00:00
2026-06-21 16:46:21 +00:00
Data ? settingsData ;
if ( settingsVersion . Version < CURRENT_SETTINGS_VERSION )
{
settingsData = await this . TryReadCurrentVersionBackupSnapshot ( ) ;
if ( settingsData is not null )
2024-08-05 19:12:52 +00:00
{
2026-06-21 16:46:21 +00:00
this . PrepareLoadedSettings ( settingsData ) ;
await this . StoreSettingsSnapshot ( settingsData , settingsPath ) ;
await this . StoreCurrentVersionBackup ( settingsData ) ;
this . logger . LogInformation ( $"Restored settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file." ) ;
return settingsData ;
2024-08-05 19:12:52 +00:00
}
2026-03-10 19:50:45 +00:00
2026-06-21 16:46:21 +00:00
this . logger . LogInformation ( "No valid current-version settings backup was found. Migrating the settings file." ) ;
settingsData = SettingsMigrations . Migrate ( this . logger , settingsVersion . Version , await File . ReadAllTextAsync ( settingsPath ) , JSON_OPTIONS ) ;
this . PrepareLoadedSettings ( settingsData ) ;
await this . StoreSettingsSnapshot ( settingsData , settingsPath ) ;
await this . StoreCurrentVersionBackup ( settingsData ) ;
2026-03-10 19:50:45 +00:00
return settingsData ;
2024-08-05 19:12:52 +00:00
}
2026-03-10 19:50:45 +00:00
2026-06-21 16:46:21 +00:00
var currentSettings = await this . TryDeserializeCurrentSettings ( settingsPath , "settings file" ) ;
if ( currentSettings . FailureReason is not SettingsWriteBlockReason . NONE )
{
this . BlockSettingsWrites ( currentSettings . FailureReason , "The current settings file could not be safely loaded. Settings writes are blocked to avoid overwriting recoverable settings." ) ;
return await this . TryReadCurrentVersionBackupSnapshotForBlockedSettings ( ) ;
}
settingsData = currentSettings . SettingsData ! ;
this . PrepareLoadedSettings ( settingsData ) ;
await this . StoreCurrentVersionBackup ( settingsData ) ;
return settingsData ;
}
private async Task < SettingsVersionReadResult > TryReadSettingsVersion ( string settingsPath )
{
try
{
await using var settingsStream = File . OpenRead ( settingsPath ) ;
using var settingsDocument = await JsonDocument . ParseAsync ( settingsStream ) ;
if ( ! settingsDocument . RootElement . TryGetProperty ( "Version" , out var versionElement ) )
{
this . logger . LogError ( $"Failed to read the version of the settings file '{settingsPath}'." ) ;
return new ( Version . UNKNOWN , SettingsWriteBlockReason . VERSION_MISSING ) ;
}
if ( versionElement . ValueKind is JsonValueKind . String & & versionElement . GetString ( ) is { } versionText )
{
if ( Enum . TryParse ( versionText , out Version stringVersion ) & & Enum . IsDefined ( stringVersion ) & & stringVersion is not Version . UNKNOWN )
return new ( stringVersion , SettingsWriteBlockReason . NONE ) ;
if ( versionText . StartsWith ( 'V' ) & & int . TryParse ( versionText [ 1. . ] , out var futureVersion ) & & futureVersion > ( int ) CURRENT_SETTINGS_VERSION )
return new ( ( Version ) futureVersion , SettingsWriteBlockReason . NONE ) ;
if ( int . TryParse ( versionText , out var numericStringVersion ) & & numericStringVersion > ( int ) CURRENT_SETTINGS_VERSION )
return new ( ( Version ) numericStringVersion , SettingsWriteBlockReason . NONE ) ;
}
if ( versionElement . ValueKind is JsonValueKind . Number & & versionElement . TryGetInt32 ( out var numericVersion ) & & numericVersion > ( int ) Version . UNKNOWN & & ( Enum . IsDefined ( typeof ( Version ) , numericVersion ) | | numericVersion > ( int ) CURRENT_SETTINGS_VERSION ) )
return new ( ( Version ) numericVersion , SettingsWriteBlockReason . NONE ) ;
}
catch ( Exception e )
{
this . logger . LogError ( e , $"Failed to read the version of the settings file '{settingsPath}'." ) ;
return new ( Version . UNKNOWN , SettingsWriteBlockReason . FILE_UNREADABLE ) ;
}
return new ( Version . UNKNOWN , SettingsWriteBlockReason . VERSION_UNKNOWN ) ;
}
private async Task < Data ? > TryReadCurrentVersionBackupSnapshot ( )
{
var backupSettingsPath = GetBackupSettingsPath ( CURRENT_SETTINGS_VERSION ) ;
if ( ! File . Exists ( backupSettingsPath ) )
{
this . logger . LogInformation ( $"The settings backup file '{backupSettingsPath}' does not exist." ) ;
return null ;
}
var backupVersion = await this . TryReadSettingsVersion ( backupSettingsPath ) ;
if ( backupVersion . FailureReason is not SettingsWriteBlockReason . NONE )
{
this . logger . LogWarning ( $"The settings backup file '{backupSettingsPath}' could not be used because its version could not be identified. Reason: '{backupVersion.FailureReason}'." ) ;
return null ;
}
if ( backupVersion . Version ! = CURRENT_SETTINGS_VERSION )
{
this . logger . LogWarning ( $"The settings backup file '{backupSettingsPath}' uses version '{backupVersion.Version}' instead of '{CURRENT_SETTINGS_VERSION}'." ) ;
return null ;
}
var backupSettings = await this . TryDeserializeCurrentSettings ( backupSettingsPath , "settings backup file" ) ;
if ( backupSettings . FailureReason is not SettingsWriteBlockReason . NONE )
{
this . logger . LogWarning ( $"The settings backup file '{backupSettingsPath}' could not be used. Reason: '{backupSettings.FailureReason}'." ) ;
return null ;
}
return backupSettings . SettingsData ;
}
private async Task < Data ? > TryReadCurrentVersionBackupSnapshotForBlockedSettings ( )
{
var settingsData = await this . TryReadCurrentVersionBackupSnapshot ( ) ;
if ( settingsData is null )
{
this . logger . LogWarning ( $"No valid current-version settings backup was found while settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'." ) ;
return null ;
}
this . PrepareLoadedSettings ( settingsData ) ;
this . logger . LogWarning ( $"Loaded settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file while settings writes remain blocked. Reason: '{this.SettingsWriteBlockReason}'." ) ;
return settingsData ;
}
private async Task < CurrentSettingsReadResult > TryDeserializeCurrentSettings ( string settingsPath , string sourceDescription )
{
try
{
var settingsData = JsonSerializer . Deserialize < Data > ( await File . ReadAllTextAsync ( settingsPath ) , JSON_OPTIONS ) ;
if ( settingsData is null )
{
this . logger . LogError ( $"Failed to parse the {sourceDescription} '{settingsPath}'." ) ;
return new ( null , SettingsWriteBlockReason . CURRENT_VERSION_INVALID ) ;
}
if ( settingsData . Version ! = CURRENT_SETTINGS_VERSION )
{
this . logger . LogError ( $"The {sourceDescription} '{settingsPath}' uses version '{settingsData.Version}' instead of '{CURRENT_SETTINGS_VERSION}'." ) ;
return new ( null , SettingsWriteBlockReason . CURRENT_VERSION_INVALID ) ;
}
return new ( settingsData , SettingsWriteBlockReason . NONE ) ;
}
catch ( Exception e )
{
this . logger . LogError ( e , $"Failed to parse the {sourceDescription} '{settingsPath}'." ) ;
return new ( null , SettingsWriteBlockReason . FILE_UNREADABLE ) ;
}
}
private void BlockSettingsWrites ( SettingsWriteBlockReason reason , string message )
{
this . SettingsWriteBlockReason = reason ;
this . logger . LogError ( $"{message} Reason: '{reason}'." ) ;
}
private void PrepareLoadedSettings ( Data settingsData )
{
//
// We filter the enabled preview features based on the preview visibility.
// This is necessary when the app starts up: some preview features may have
// been disabled or released from the last time the app was started.
//
settingsData . App . EnabledPreviewFeatures = settingsData . App . PreviewVisibility . FilterPreviewFeatures ( settingsData . App . EnabledPreviewFeatures ) ;
2024-04-19 19:19:13 +00:00
}
2024-04-20 15:06:50 +00:00
2024-05-04 08:55:00 +00:00
/// <summary>
/// Stores the settings to the file system.
/// </summary>
2024-04-20 15:06:50 +00:00
public async Task StoreSettings ( )
2024-04-19 19:19:13 +00:00
{
if ( ! this . IsSetUp )
2024-09-01 18:10:03 +00:00
{
this . logger . LogWarning ( "Cannot store settings, because the configuration is not set up yet." ) ;
2024-04-19 19:19:13 +00:00
return ;
2024-09-01 18:10:03 +00:00
}
2026-06-21 16:46:21 +00:00
if ( this . SettingsWriteBlocked )
{
this . logger . LogWarning ( $"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'." ) ;
return ;
}
2024-04-19 19:19:13 +00:00
var settingsPath = Path . Combine ( ConfigDirectory ! , SETTINGS_FILENAME ) ;
2026-06-21 16:46:21 +00:00
await this . StoreSettingsSnapshot ( this . ConfigurationData , settingsPath ) ;
await this . StoreCurrentVersionBackup ( this . ConfigurationData ) ;
}
private static string GetBackupSettingsFilename ( Version version ) = > $"settings.{version.ToString().ToLowerInvariant()}.json" ;
private static string GetBackupSettingsPath ( Version version ) = > Path . Combine ( ConfigDirectory ! , GetBackupSettingsFilename ( version ) ) ;
private async Task StoreCurrentVersionBackup ( Data settingsData )
{
if ( settingsData . Version ! = CURRENT_SETTINGS_VERSION )
{
this . logger . LogWarning ( $"Skipping settings backup because the settings version '{settingsData.Version}' is not the current version '{CURRENT_SETTINGS_VERSION}'." ) ;
return ;
}
var backupSettingsPath = GetBackupSettingsPath ( CURRENT_SETTINGS_VERSION ) ;
await this . StoreSettingsSnapshot ( settingsData , backupSettingsPath ) ;
this . logger . LogInformation ( $"Stored the settings backup file '{backupSettingsPath}'." ) ;
}
private async Task StoreSettingsSnapshot ( Data settingsData , string settingsPath )
{
2024-04-20 15:06:50 +00:00
if ( ! Directory . Exists ( ConfigDirectory ) )
2024-09-01 18:10:03 +00:00
{
this . logger . LogInformation ( "Creating the configuration directory." ) ;
2024-04-20 15:06:50 +00:00
Directory . CreateDirectory ( ConfigDirectory ! ) ;
2024-09-01 18:10:03 +00:00
}
2026-06-21 16:46:21 +00:00
var settingsJson = JsonSerializer . Serialize ( settingsData , JSON_OPTIONS ) ;
2025-01-01 16:15:04 +00:00
var tempFile = Path . GetTempFileName ( ) ;
await File . WriteAllTextAsync ( tempFile , settingsJson ) ;
2024-09-01 18:10:03 +00:00
2025-01-01 16:15:04 +00:00
File . Move ( tempFile , settingsPath , true ) ;
2026-06-21 16:46:21 +00:00
this . logger . LogInformation ( $"Stored the settings to '{settingsPath}'." ) ;
2024-04-19 19:19:13 +00:00
}
2024-06-01 17:55:12 +00:00
2024-08-05 19:12:52 +00:00
public void InjectSpellchecking ( Dictionary < string , object? > attributes ) = > attributes [ "spellcheck" ] = this . ConfigurationData . App . EnableSpellchecking ? "true" : "false" ;
2024-09-04 13:44:23 +00:00
2024-09-14 17:20:33 +00:00
public ConfidenceLevel GetMinimumConfidenceLevel ( Tools . Components component )
{
var minimumLevel = ConfidenceLevel . NONE ;
2026-06-21 09:52:02 +00:00
var enforceGlobalMinimumConfidence = this . ConfigurationData . Confidence is { EnforceGlobalMinimumConfidence : true , GlobalMinimumConfidence : not ConfidenceLevel . NONE and not ConfidenceLevel . UNKNOWN } ;
2024-09-14 17:20:33 +00:00
if ( enforceGlobalMinimumConfidence )
2026-06-21 09:52:02 +00:00
minimumLevel = this . ConfigurationData . Confidence . GlobalMinimumConfidence ;
2024-09-14 17:20:33 +00:00
var componentMinimumLevel = component . MinimumConfidence ( this ) ;
if ( componentMinimumLevel > minimumLevel )
minimumLevel = componentMinimumLevel ;
return minimumLevel ;
}
2025-04-12 19:13:33 +00:00
/// <summary>
/// Checks if the given plugin is enabled.
/// </summary>
/// <param name="plugin">The plugin to check.</param>
/// <returns>True, when the plugin is enabled, false otherwise.</returns>
2025-06-01 19:14:21 +00:00
public bool IsPluginEnabled ( IPluginMetadata plugin ) = > plugin . Type is PluginType . CONFIGURATION | | this . ConfigurationData . EnabledPlugins . Contains ( plugin . Id ) ;
2025-03-29 17:40:17 +00:00
2025-04-12 19:13:33 +00:00
/// <summary>
/// Returns the active language plugin.
/// </summary>
/// <returns>The active language plugin.</returns>
public async Task < ILanguagePlugin > GetActiveLanguagePlugin ( )
{
switch ( this . ConfigurationData . App . LanguageBehavior )
{
case LangBehavior . AUTO :
var languageCode = await this . rustService . ReadUserLanguage ( ) ;
2026-02-16 11:13:36 +00:00
var languagePlugins = PluginFactory . RunningPlugins . OfType < ILanguagePlugin > ( ) . ToList ( ) ;
if ( ! string . IsNullOrWhiteSpace ( languageCode ) )
2025-04-27 07:06:05 +00:00
{
2026-02-16 11:13:36 +00:00
var exactMatch = languagePlugins . FirstOrDefault ( x = > string . Equals ( x . IETFTag , languageCode , StringComparison . OrdinalIgnoreCase ) ) ;
if ( exactMatch is not null )
return exactMatch ;
var primaryLanguage = GetPrimaryLanguage ( languageCode ) ;
if ( ! string . IsNullOrWhiteSpace ( primaryLanguage ) )
{
var primaryLanguageMatch = languagePlugins
. Where ( x = > string . Equals ( GetPrimaryLanguage ( x . IETFTag ) , primaryLanguage , StringComparison . OrdinalIgnoreCase ) )
. OrderBy ( x = > x . IETFTag , StringComparer . OrdinalIgnoreCase )
. FirstOrDefault ( ) ;
if ( primaryLanguageMatch is not null )
{
this . logger . LogWarning ( $"No exact language plugin found for '{languageCode}'. Use language fallback '{primaryLanguageMatch.IETFTag}'." ) ;
return primaryLanguageMatch ;
}
}
2025-04-27 07:06:05 +00:00
}
2026-02-16 11:13:36 +00:00
this . logger . LogWarning ( $"The language plugin for the language '{languageCode}' (normalized='{languageCode}') is not available." ) ;
2025-04-12 19:13:33 +00:00
return PluginFactory . BaseLanguage ;
case LangBehavior . MANUAL :
var pluginId = this . ConfigurationData . App . LanguagePluginId ;
var plugin = PluginFactory . RunningPlugins . FirstOrDefault ( x = > x . Id = = pluginId ) ;
if ( plugin is null )
{
this . logger . LogWarning ( $"The chosen language plugin (id='{pluginId}') is not available." ) ;
return PluginFactory . BaseLanguage ;
}
if ( plugin is ILanguagePlugin chosenLangPlugin )
return chosenLangPlugin ;
2025-04-27 07:06:05 +00:00
2025-04-12 19:13:33 +00:00
this . logger . LogError ( "The chosen language plugin is not a language plugin." ) ;
return PluginFactory . BaseLanguage ;
}
this . logger . LogError ( "The language behavior is unknown." ) ;
return PluginFactory . BaseLanguage ;
}
2026-02-16 11:13:36 +00:00
private static string GetPrimaryLanguage ( string localeTag )
{
if ( string . IsNullOrWhiteSpace ( localeTag ) )
return string . Empty ;
var separatorIndex = localeTag . IndexOf ( '-' ) ;
if ( separatorIndex < 0 )
return localeTag ;
return localeTag [ . . separatorIndex ] ;
}
2025-04-12 19:13:33 +00:00
2025-02-23 14:05:29 +00:00
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
public Provider GetPreselectedProvider ( Tools . Components component , string? currentProviderId = null , bool usePreselectionBeforeCurrentProvider = false )
2024-09-04 13:44:23 +00:00
{
2024-09-14 17:20:33 +00:00
var minimumLevel = this . GetMinimumConfidenceLevel ( component ) ;
// When there is only one provider, and it has a confidence level that is high enough, we return it:
2024-09-13 21:29:19 +00:00
if ( this . ConfigurationData . Providers . Count = = 1 & & this . ConfigurationData . Providers [ 0 ] . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
2024-09-04 13:44:23 +00:00
return this . ConfigurationData . Providers [ 0 ] ;
2025-02-23 14:05:29 +00:00
// Is there a current provider with a sufficiently high confidence level?
2025-08-26 08:59:56 +00:00
var currentProvider = Provider . NONE ;
2025-02-23 14:05:29 +00:00
if ( currentProviderId is not null & & ! string . IsNullOrWhiteSpace ( currentProviderId ) )
2024-11-23 12:04:02 +00:00
{
2025-02-23 14:05:29 +00:00
var currentProviderProbe = this . ConfigurationData . Providers . FirstOrDefault ( x = > x . Id = = currentProviderId ) ;
2025-08-26 08:59:56 +00:00
if ( currentProviderProbe is not null & & currentProviderProbe . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
2025-02-23 14:05:29 +00:00
currentProvider = currentProviderProbe ;
2024-11-23 12:04:02 +00:00
}
2025-02-23 14:05:29 +00:00
// Is there a component-preselected provider with a sufficiently high confidence level?
2025-08-26 08:59:56 +00:00
var preselectedProvider = Provider . NONE ;
2025-02-23 14:05:29 +00:00
var preselectedProviderProbe = component . PreselectedProvider ( this ) ;
2025-08-26 08:59:56 +00:00
if ( preselectedProviderProbe ! = Provider . NONE & & preselectedProviderProbe . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel )
2025-02-23 14:05:29 +00:00
preselectedProvider = preselectedProviderProbe ;
//
// Case: The preselected provider should be used before the current provider,
// and the preselected provider is available and has a confidence level
// that is high enough.
//
2025-08-26 08:59:56 +00:00
if ( usePreselectionBeforeCurrentProvider & & preselectedProvider ! = Provider . NONE )
2025-02-23 14:05:29 +00:00
return preselectedProvider ;
//
// Case: The current provider is available and has a confidence level that is
// high enough.
//
2025-08-26 08:59:56 +00:00
if ( currentProvider ! = Provider . NONE )
2025-02-23 14:05:29 +00:00
return currentProvider ;
//
// Case: The current provider should be used before the preselected provider,
// but the current provider is not available or does not have a confidence
// level that is high enough. The preselected provider is available and
// has a confidence level that is high enough.
//
2025-08-26 08:59:56 +00:00
if ( preselectedProvider ! = Provider . NONE )
2024-09-14 17:20:33 +00:00
return preselectedProvider ;
// When there is an app-wide preselected provider, and it has a confidence level that is high enough, we return it:
2025-08-26 08:59:56 +00:00
return this . ConfigurationData . Providers . FirstOrDefault ( x = > x . Id = = this . ConfigurationData . App . PreselectedProvider & & x . UsedLLMProvider . GetConfidence ( this ) . Level > = minimumLevel ) ? ? Provider . NONE ;
2024-09-04 13:44:23 +00:00
}
2024-09-08 19:01:51 +00:00
2026-04-16 07:09:05 +00:00
[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 ;
Provider ? FindProviderById ( string? providerId )
{
if ( string . IsNullOrWhiteSpace ( providerId ) )
return null ;
var provider = this . ConfigurationData . Providers . FirstOrDefault ( x = > x . Id = = providerId ) ;
return provider is not null & & IsSelectableProvider ( provider ) ? provider : null ;
}
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 ;
}
2024-09-08 19:01:51 +00:00
public Profile GetPreselectedProfile ( Tools . Components component )
{
2026-03-14 14:40:07 +00:00
var preselection = component . GetProfilePreselection ( this ) ;
if ( preselection . DoNotPreselectProfile )
return Profile . NO_PROFILE ;
if ( preselection . UseSpecificProfile )
2026-06-10 19:01:27 +00:00
return this . GetProfileById ( preselection . SpecificProfileId ) ;
2026-03-14 14:40:07 +00:00
var appPreselection = ProfilePreselection . FromStoredValue ( this . ConfigurationData . App . PreselectedProfile ) ;
if ( appPreselection . DoNotPreselectProfile | | ! appPreselection . UseSpecificProfile )
return Profile . NO_PROFILE ;
2026-06-10 19:01:27 +00:00
return this . GetProfileById ( appPreselection . SpecificProfileId ) ;
2026-03-14 14:40:07 +00:00
}
public Profile GetAppPreselectedProfile ( )
{
var appPreselection = ProfilePreselection . FromStoredValue ( this . ConfigurationData . App . PreselectedProfile ) ;
if ( appPreselection . DoNotPreselectProfile | | ! appPreselection . UseSpecificProfile )
return Profile . NO_PROFILE ;
2026-06-10 19:01:27 +00:00
return this . GetProfileById ( appPreselection . SpecificProfileId ) ;
2024-09-08 19:01:51 +00:00
}
2025-05-24 10:27:00 +00:00
public ChatTemplate GetPreselectedChatTemplate ( Tools . Components component )
{
var preselection = component . PreselectedChatTemplate ( this ) ;
2025-08-18 18:40:52 +00:00
if ( preselection ! = ChatTemplate . NO_CHAT_TEMPLATE )
2025-05-24 10:27:00 +00:00
return preselection ;
2026-06-10 19:01:27 +00:00
return this . GetChatTemplateById ( this . ConfigurationData . App . PreselectedChatTemplate ) ;
}
public Profile GetProfileById ( string? profileId )
{
if ( string . IsNullOrWhiteSpace ( profileId ) )
return Profile . NO_PROFILE ;
if ( string . Equals ( profileId , Profile . NO_PROFILE . Id , StringComparison . OrdinalIgnoreCase ) )
return Profile . NO_PROFILE ;
return this . ConfigurationData . Profiles . FirstOrDefault ( x = > x . Id . Equals ( profileId , StringComparison . OrdinalIgnoreCase ) ) ? ? Profile . NO_PROFILE ;
}
public ChatTemplate GetChatTemplateById ( string? chatTemplateId )
{
if ( string . IsNullOrWhiteSpace ( chatTemplateId ) )
return ChatTemplate . NO_CHAT_TEMPLATE ;
if ( string . Equals ( chatTemplateId , ChatTemplate . NO_CHAT_TEMPLATE . Id , StringComparison . OrdinalIgnoreCase ) )
return ChatTemplate . NO_CHAT_TEMPLATE ;
return this . ConfigurationData . ChatTemplates . FirstOrDefault ( x = > x . Id . Equals ( chatTemplateId , StringComparison . OrdinalIgnoreCase ) ) ? ? ChatTemplate . NO_CHAT_TEMPLATE ;
2025-05-24 10:27:00 +00:00
}
2024-09-11 21:08:02 +00:00
2024-09-13 19:50:00 +00:00
public ConfidenceLevel GetConfiguredConfidenceLevel ( LLMProviders llmProvider )
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
if ( llmProvider is LLMProviders . NONE )
2024-09-11 21:08:02 +00:00
return ConfidenceLevel . NONE ;
2026-06-21 09:52:02 +00:00
switch ( this . ConfigurationData . Confidence . ConfidenceScheme )
2024-09-11 21:08:02 +00:00
{
2025-02-27 11:43:19 +00:00
case ConfidenceSchemes . TRUST_ALL :
return llmProvider switch
{
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
_ = > ConfidenceLevel . MEDIUM ,
} ;
2024-09-11 21:08:02 +00:00
case ConfidenceSchemes . TRUST_USA_EUROPE :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
2025-02-27 11:43:19 +00:00
LLMProviders . DEEP_SEEK = > ConfidenceLevel . LOW ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . MEDIUM ,
} ;
case ConfidenceSchemes . TRUST_USA :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
LLMProviders . MISTRAL = > ConfidenceLevel . LOW ,
2025-02-27 11:43:19 +00:00
LLMProviders . HELMHOLTZ = > ConfidenceLevel . LOW ,
LLMProviders . GWDG = > ConfidenceLevel . LOW ,
LLMProviders . DEEP_SEEK = > ConfidenceLevel . LOW ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . MEDIUM ,
} ;
case ConfidenceSchemes . TRUST_EUROPE :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
LLMProviders . MISTRAL = > ConfidenceLevel . MEDIUM ,
2025-02-27 11:43:19 +00:00
LLMProviders . HELMHOLTZ = > ConfidenceLevel . MEDIUM ,
LLMProviders . GWDG = > ConfidenceLevel . MEDIUM ,
_ = > ConfidenceLevel . LOW ,
} ;
case ConfidenceSchemes . TRUST_ASIA :
return llmProvider switch
{
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
LLMProviders . DEEP_SEEK = > ConfidenceLevel . MEDIUM ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . LOW ,
} ;
case ConfidenceSchemes . LOCAL_TRUST_ONLY :
2024-09-13 19:50:00 +00:00
return llmProvider switch
2024-09-11 21:08:02 +00:00
{
2024-09-13 19:50:00 +00:00
LLMProviders . SELF_HOSTED = > ConfidenceLevel . HIGH ,
2024-09-11 21:08:02 +00:00
_ = > ConfidenceLevel . VERY_LOW ,
} ;
case ConfidenceSchemes . CUSTOM :
2026-06-21 09:52:02 +00:00
return this . ConfigurationData . Confidence . CustomConfidenceScheme . GetValueOrDefault ( llmProvider , ConfidenceLevel . UNKNOWN ) ;
2024-09-11 21:08:02 +00:00
default :
return ConfidenceLevel . UNKNOWN ;
}
}
2025-06-27 20:52:34 +00:00
2025-08-09 17:29:43 +00:00
public static string ToSettingName < TIn , TOut > ( Expression < Func < TIn , TOut > > propertyExpression )
2025-06-27 20:52:34 +00:00
{
MemberExpression ? memberExpr ;
// Handle the case where the expression is a unary expression (e.g., when using Convert):
if ( propertyExpression . Body is UnaryExpression { NodeType : ExpressionType . Convert } unaryExpr )
memberExpr = unaryExpr . Operand as MemberExpression ;
else
memberExpr = propertyExpression . Body as MemberExpression ;
if ( memberExpr is null )
throw new ArgumentException ( "Expression must be a property access" , nameof ( propertyExpression ) ) ;
// Return the full name of the property, including the class name:
2025-08-09 17:29:43 +00:00
return $"{typeof(TIn).Name}.{memberExpr.Member.Name}" ;
2025-06-27 20:52:34 +00:00
}
2026-06-21 13:16:37 +00:00
}