Multiple Profiles can now be selected

This commit is contained in:
Peer Hogeterp committed 2026-09-07 16:09:00 +02:00
1 parent d043fbc8f0
commit cefc5464d5
89 files changed
+1360 -503

No files matched your search

@@ -33,6 +33,11 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
/// </summary>
public required TValue Default { get; init; }
/// <summary>
/// Whether a managed user-value snapshot may restore JSON null for this setting.
/// </summary>
public bool AllowNullSnapshot { get; init; }
/// <summary>
/// The additive value contributions, one per contributing configuration plugin.
/// </summary>
@@ -70,10 +75,10 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
try
{
var value = JsonSerializer.Deserialize<TValue>(json, SettingsManager.JSON_OPTIONS);
if (value is null)
if (value is null && !this.AllowNullSnapshot)
return false;
this.SetValue(value);
this.SetValue(value!);
return true;
}
catch (Exception e)
@@ -116,4 +121,4 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
return default!;
}
}
}
@@ -238,17 +238,8 @@ public static class ConfigurationSelectDataFactory
public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles)
{
foreach (var profile in profiles.GetAllProfiles())
yield return new(profile.GetSafeName(), profile.Id);
}
public static IEnumerable<ConfigurationSelectData<ProfilePreselection>> GetComponentProfilesData(IEnumerable<Profile> profiles)
{
yield return new(TB("Use app default profile"), ProfilePreselection.AppDefault);
yield return new(Profile.NO_PROFILE.GetSafeName(), ProfilePreselection.NoProfile);
foreach (var profile in profiles)
yield return new(profile.GetSafeName(), ProfilePreselection.Specific(profile.Id));
yield return new(profile.GetSafeName(), profile.Id);
}
public static IEnumerable<ConfigurationSelectData<string>> GetTranscriptionProvidersData(IEnumerable<TranscriptionProvider> transcriptionProviders)
@@ -11,7 +11,7 @@ public sealed class Data
/// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available.
/// </summary>
public Version Version { get; init; } = Version.V6;
public Version Version { get; init; } = Version.V7;
/// <summary>
/// List of configured providers.
@@ -65,5 +65,5 @@ public sealed class DataAgenda
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}
@@ -98,9 +98,9 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary>
/// Should we preselect a profile for the entire app?
/// Which profiles should be preselected for the entire app?
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfile, string.Empty);
public HashSet<string> PreselectedProfileIds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfileIds, []);
/// <summary>
/// Should we preselect a chat template for the entire app?
@@ -52,10 +52,10 @@ public sealed class DataBiasOfTheDay
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public HashSet<string>? PreselectedProfileIds { get; set; }
/// <summary>
/// Preselect a provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
}
}
@@ -42,9 +42,9 @@ public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection =
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary>
/// Preselect a profile?
/// Which profiles should be preselected? Null uses the app default.
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfile, string.Empty);
public HashSet<string>? PreselectedProfileIds { get; set; } = ManagedConfiguration.RegisterProfilePreselection(configSelection, n => n.PreselectedProfileIds);
/// <summary>
/// Preselect a chat template?
@@ -97,4 +97,4 @@ public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection =
/// Should we show the latest message after loading? When false, we show the first (aka oldest) message.
/// </summary>
public bool ShowLatestMessageAfterLoading { get; set; } = true;
}
}
@@ -38,5 +38,5 @@ public sealed class DataCoding
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}
@@ -77,9 +77,9 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// Which profiles should be preselected? Null uses the app default.
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public HashSet<string>? PreselectedProfileIds { get; set; }
/// <summary>
/// Hide the policy definition section in the UI?
@@ -135,9 +135,31 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
if (table.TryGetValue("PreselectedProvider", out var providerValue) && providerValue.TryRead<string>(out var providerId))
preselectedProvider = providerId;
var preselectedProfile = string.Empty;
if (table.TryGetValue("PreselectedProfile", out var profileValue) && profileValue.TryRead<string>(out var profileId))
preselectedProfile = profileId;
var hasLegacyProfile = table.TryGetValue("PreselectedProfile", out var profileValue);
var hasProfileIds = table.TryGetValue("PreselectedProfileIds", out var profileIdsValue);
if (hasLegacyProfile && hasProfileIds)
{
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains both PreselectedProfile and PreselectedProfileIds.", idx);
return false;
}
HashSet<string>? preselectedProfileIds = null;
if (hasLegacyProfile)
{
if (!profileValue.TryRead<string>(out var profileId) || !TryNormalizeLegacyProfileId(profileId, out preselectedProfileIds))
{
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains an invalid PreselectedProfile.", idx);
return false;
}
}
else if (hasProfileIds)
{
if (!profileIdsValue.TryRead<LuaTable>(out var profileIdsTable) || !TryReadProfileIds(profileIdsTable, out preselectedProfileIds))
{
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains invalid PreselectedProfileIds. Expected unique, non-empty GUIDs.", idx);
return false;
}
}
var hidePolicyDefinition = false;
if (table.TryGetValue("HidePolicyDefinition", out var hideValue) && hideValue.TryRead<bool>(out var hide))
@@ -171,7 +193,7 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
MinimumProviderConfidence = minimumConfidence,
AllowedToolIds = allowedToolIds,
PreselectedProvider = preselectedProvider,
PreselectedProfile = preselectedProfile,
PreselectedProfileIds = preselectedProfileIds,
HidePolicyDefinition = hidePolicyDefinition,
IsProtected = true,
IsEnterpriseConfiguration = true,
@@ -180,4 +202,36 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
return true;
}
}
private static bool TryNormalizeLegacyProfileId(string profileId, out HashSet<string>? profileIds)
{
profileIds = null;
if (string.IsNullOrWhiteSpace(profileId))
return true;
if (!Guid.TryParse(profileId, out var parsed))
return false;
profileIds = parsed == Guid.Empty ? [] : [parsed.ToString()];
return true;
}
private static bool TryReadProfileIds(LuaTable table, out HashSet<string>? profileIds)
{
var parsedIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var index = 1; index <= table.ArrayLength; index++)
{
if (!table[index].TryRead<string>(out var profileId) ||
!Guid.TryParse(profileId, out var parsed) ||
parsed == Guid.Empty ||
!parsedIds.Add(parsed.ToString()))
{
profileIds = null;
return false;
}
}
profileIds = parsedIds;
return true;
}
}
@@ -48,5 +48,5 @@ public sealed class DataEMail
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}
@@ -32,5 +32,5 @@ public sealed class DataERI
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}
@@ -37,5 +37,5 @@ public class DataLegalCheck
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}
@@ -32,5 +32,5 @@ public sealed class DataMyTasks
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}
@@ -13,7 +13,7 @@ public class DataSlideBuilder
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public HashSet<string>? PreselectedProfileIds { get; set; }
/// <summary>
/// Preselect a Slide Builder provider?
@@ -59,4 +59,4 @@ public class DataSlideBuilder
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
}
}
@@ -19,9 +19,9 @@ public sealed class DataVisualBriefing(Expression<Func<Data, DataVisualBriefing>
}
/// <summary>
/// Gets or sets the preselected profile identifier.
/// Gets or sets the preselected profile identifiers. Null uses the app default.
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProfile, string.Empty);
public HashSet<string>? PreselectedProfileIds { get; set; } = ManagedConfiguration.RegisterProfilePreselection(configSelection, value => value.PreselectedProfileIds);
/// <summary>
/// Gets or sets the preselected provider identifier.
@@ -72,4 +72,4 @@ public sealed class DataVisualBriefing(Expression<Func<Data, DataVisualBriefing>
/// Gets or sets the minimum confidence accepted for the selected provider.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
}
}
@@ -0,0 +1,191 @@
using System.Linq.Expressions;
using AIStudio.Settings.DataModel;
using LuaTable = Lua.LuaTable;
using LuaValueType = Lua.LuaValueType;
namespace AIStudio.Settings;
public static partial class ManagedConfiguration
{
public static bool TryProcessProfileIds<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<string>>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
ISet<string> configuredValue = configMeta.Default;
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
successful = TryReadProfileIds(valueTable, out var values);
configuredValue = values;
}
return HandleParsedScalarValue(
configPluginId,
dryRun,
successful,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
public static bool TryProcessLegacyProfileIds<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<string>>> propertyExpression,
string legacySettingName,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGet(configSelection, propertyExpression, out var configMeta) ||
!settings.TryGetValue(legacySettingName, out var legacyValue) ||
!legacyValue.TryRead<string>(out var legacyProfileId))
return false;
if (!string.IsNullOrWhiteSpace(legacyProfileId) && !Guid.TryParse(legacyProfileId, out _))
return false;
ISet<string> configuredValue = Guid.TryParse(legacyProfileId, out var profileId) && profileId != Guid.Empty
? new HashSet<string> { profileId.ToString() }
: new HashSet<string>();
return HandleParsedScalarValue(
configPluginId,
dryRun,
true,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
public static HashSet<string>? RegisterProfilePreselection<TClass>(
Expression<Func<Data, TClass>>? configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression)
{
if (configSelection is null)
return null;
var configPath = Path(configSelection, propertyExpression);
if (!METADATA.ContainsKey(configPath))
{
METADATA[configPath] = new ConfigMeta<TClass, HashSet<string>?>(configSelection, propertyExpression)
{
Default = null,
AllowNullSnapshot = true,
};
}
return null;
}
public static bool TryGetProfilePreselection<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression,
out ConfigMeta<TClass, HashSet<string>?> configMeta)
{
var configPath = Path(configSelection, propertyExpression);
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, HashSet<string>?> meta)
{
meta.RestoreLockedConfiguration();
configMeta = meta;
return true;
}
configMeta = new NoConfig<TClass, HashSet<string>?>(configSelection, propertyExpression)
{
Default = null,
};
return false;
}
public static bool TryProcessProfilePreselection<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGetProfilePreselection(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
HashSet<string>? configuredValue = null;
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
successful = TryReadProfileIds(valueTable, out var values);
if (successful)
configuredValue = values;
}
if (dryRun)
return successful;
return HandleParsedScalarValue(
configPluginId,
dryRun,
successful,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
public static bool TryProcessLegacyProfilePreselection<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression,
string legacySettingName,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGetProfilePreselection(configSelection, propertyExpression, out var configMeta) ||
!settings.TryGetValue(legacySettingName, out var legacyValue) ||
!legacyValue.TryRead<string>(out var legacyProfileId))
return false;
HashSet<string>? configuredValue;
if (string.IsNullOrWhiteSpace(legacyProfileId))
configuredValue = null;
else if (!Guid.TryParse(legacyProfileId, out var parsedProfileId))
return false;
else
configuredValue = parsedProfileId == Guid.Empty ? [] : [parsedProfileId.ToString()];
return HandleParsedScalarValue(
configPluginId,
dryRun,
true,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
private static bool TryReadProfileIds(LuaTable valueTable, out HashSet<string> values)
{
values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var index = 1; index <= valueTable.ArrayLength; index++)
{
if (!valueTable[index].TryRead<string>(out var value) ||
!Guid.TryParse(value, out var profileId) ||
profileId == Guid.Empty ||
!values.Add(profileId.ToString()))
return false;
}
return true;
}
}
+20 -1
View File
@@ -94,6 +94,25 @@ public record Profile(
{actions}
""";
}
public static string ToSystemPrompt(IReadOnlyList<Profile> profiles)
{
if (profiles.Count == 0)
return string.Empty;
var profileSections = profiles.Select((profile, index) =>
$"""
## Profile {index + 1}: {profile.Name}
{profile.ToSystemPrompt()}
""");
return $"""
The user selected the following profiles. Combine them as equally important context. If their instructions conflict, resolve the conflict as well as possible.
{string.Join("\n\n---\n\n", profileSections)}
""";
}
public static bool TryParseProfileTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template)
{
@@ -151,4 +170,4 @@ public record Profile(
}
""";
}
}
}
@@ -4,52 +4,54 @@ public readonly record struct ProfilePreselection
{
public ProfilePreselectionMode Mode { get; }
public string SpecificProfileId { get; }
public IReadOnlySet<string> SpecificProfileIds { get; }
public bool UseAppDefault => this.Mode == ProfilePreselectionMode.USE_APP_DEFAULT;
public bool DoNotPreselectProfile => this.Mode == ProfilePreselectionMode.USE_NO_PROFILE;
public bool DoNotPreselectProfiles => this.Mode == ProfilePreselectionMode.USE_NO_PROFILES;
public bool UseSpecificProfile => this.Mode == ProfilePreselectionMode.USE_SPECIFIC_PROFILE;
public bool UseSpecificProfiles => this.Mode == ProfilePreselectionMode.USE_SPECIFIC_PROFILES;
public static ProfilePreselection AppDefault => new(ProfilePreselectionMode.USE_APP_DEFAULT, string.Empty);
public static ProfilePreselection AppDefault => new(ProfilePreselectionMode.USE_APP_DEFAULT, new HashSet<string>());
public static ProfilePreselection NoProfile => new(ProfilePreselectionMode.USE_NO_PROFILE, Profile.NO_PROFILE.Id);
public static ProfilePreselection NoProfiles => new(ProfilePreselectionMode.USE_NO_PROFILES, new HashSet<string>());
private ProfilePreselection(ProfilePreselectionMode mode, string specificProfileId)
private ProfilePreselection(ProfilePreselectionMode mode, IReadOnlySet<string> specificProfileIds)
{
this.Mode = mode;
this.SpecificProfileId = specificProfileId;
this.SpecificProfileIds = specificProfileIds;
}
public static ProfilePreselection Specific(string profileId)
public static ProfilePreselection Specific(IEnumerable<string> profileIds)
{
if (string.IsNullOrWhiteSpace(profileId))
throw new ArgumentException("A specific profile preselection requires a profile ID.", nameof(profileId));
var normalizedIds = profileIds
.Where(profileId => !string.IsNullOrWhiteSpace(profileId) && !profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Use NoProfile for the NO_PROFILE selection.", nameof(profileId));
if (normalizedIds.Count == 0)
throw new ArgumentException("A specific profile preselection requires at least one profile ID.", nameof(profileIds));
return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, profileId);
return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILES, normalizedIds);
}
public static ProfilePreselection FromStoredValue(string? storedValue)
public static ProfilePreselection FromStoredValue(IEnumerable<string>? storedValue)
{
if (string.IsNullOrWhiteSpace(storedValue))
if (storedValue is null)
return AppDefault;
if (storedValue.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
return NoProfile;
var profileIds = storedValue
.Where(profileId => !string.IsNullOrWhiteSpace(profileId) && !profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, storedValue);
return profileIds.Count == 0 ? NoProfiles : Specific(profileIds);
}
public static implicit operator string(ProfilePreselection preselection) => preselection.Mode switch
public static implicit operator HashSet<string>?(ProfilePreselection preselection) => preselection.Mode switch
{
ProfilePreselectionMode.USE_APP_DEFAULT => string.Empty,
ProfilePreselectionMode.USE_NO_PROFILE => Profile.NO_PROFILE.Id,
ProfilePreselectionMode.USE_SPECIFIC_PROFILE => preselection.SpecificProfileId,
_ => string.Empty,
ProfilePreselectionMode.USE_APP_DEFAULT => null,
ProfilePreselectionMode.USE_NO_PROFILES => [],
ProfilePreselectionMode.USE_SPECIFIC_PROFILES => [..preselection.SpecificProfileIds],
_ => null,
};
}
}
@@ -3,6 +3,6 @@ namespace AIStudio.Settings;
public enum ProfilePreselectionMode
{
USE_APP_DEFAULT,
USE_NO_PROFILE,
USE_SPECIFIC_PROFILE,
}
USE_NO_PROFILES,
USE_SPECIFIC_PROFILES,
}
@@ -19,7 +19,7 @@ public sealed class SettingsManager
public readonly record struct ToolMinimumProviderConfidenceResolution(ConfidenceLevel ConfidenceLevel, string Source);
private const string SETTINGS_FILENAME = "settings.json";
private const Version CURRENT_SETTINGS_VERSION = Version.V6;
private const Version CURRENT_SETTINGS_VERSION = Version.V7;
private readonly record struct SettingsVersionReadResult(Version Version, SettingsWriteBlockReason FailureReason);
@@ -28,6 +28,7 @@ public sealed class SettingsManager
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
Converters = { new TolerantEnumConverter() },
};
@@ -170,7 +171,10 @@ public sealed class SettingsManager
try
{
await using var settingsStream = File.OpenRead(settingsPath);
using var settingsDocument = await JsonDocument.ParseAsync(settingsStream);
using var settingsDocument = await JsonDocument.ParseAsync(settingsStream, new JsonDocumentOptions
{
AllowTrailingCommas = JSON_OPTIONS.AllowTrailingCommas,
});
if(!settingsDocument.RootElement.TryGetProperty("Version", out var versionElement))
{
this.logger.LogError($"Failed to read the version of the settings file '{settingsPath}'.");
@@ -731,29 +735,34 @@ public sealed class SettingsManager
return this.ConfigurationData.TranscriptionProviders.FirstOrDefault(x => x.Id.Equals(transcriptionProviderId, StringComparison.OrdinalIgnoreCase)) ?? TranscriptionProvider.NONE;
}
public Profile GetPreselectedProfile(Tools.Components component)
public IReadOnlyList<Profile> GetPreselectedProfiles(Tools.Components component)
{
var preselection = component.GetProfilePreselection(this);
if (preselection.DoNotPreselectProfile)
return Profile.NO_PROFILE;
if (preselection.DoNotPreselectProfiles)
return [];
if (preselection.UseSpecificProfile)
return this.GetProfileById(preselection.SpecificProfileId);
if (preselection.UseSpecificProfiles)
return this.ResolveProfiles(preselection.SpecificProfileIds);
var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile);
if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile)
return Profile.NO_PROFILE;
return this.GetProfileById(appPreselection.SpecificProfileId);
return this.GetAppPreselectedProfiles();
}
public Profile GetAppPreselectedProfile()
{
var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile);
if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile)
return Profile.NO_PROFILE;
public IReadOnlyList<Profile> GetAppPreselectedProfiles() => this.ResolveProfiles(this.ConfigurationData.App.PreselectedProfileIds);
return this.GetProfileById(appPreselection.SpecificProfileId);
public IReadOnlyList<Profile> ResolveProfiles(IEnumerable<string>? profileIds)
{
if (profileIds is null)
return [];
var requestedIds = profileIds
.Where(profileId => !string.IsNullOrWhiteSpace(profileId))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
requestedIds.Remove(Profile.NO_PROFILE.Id);
return this.ConfigurationData.Profiles
.Where(profile => requestedIds.Contains(profile.Id))
.DistinctBy(profile => profile.Id, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public ChatTemplate GetPreselectedChatTemplate(Tools.Components component)
@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Settings.DataModel;
using AIStudio.Settings.DataModel.PreviousModels;
@@ -73,16 +74,19 @@ public static class SettingsMigrations
return MigrateV5ToV6(logger, configV5);
case Version.V6:
return MigrateV6ToV7(logger, configData, jsonOptions);
default:
logger.LogInformation("No configuration migration is needed.");
var configV6 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV6 is null)
var configV7 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV7 is null)
{
logger.LogError("Failed to parse the v6 configuration. Using default values.");
logger.LogError("Failed to parse the v7 configuration. Using default values.");
return new();
}
return configV6;
return configV7;
}
}
@@ -266,7 +270,7 @@ public static class SettingsMigrations
logger.LogInformation("Migrating from v5 to v6...");
return new()
{
Version = Version.V6,
Version = Version.V7,
Providers = previousConfig.Providers,
Confidence = new(x => x.Confidence)
{
@@ -319,4 +323,104 @@ public static class SettingsMigrations
I18N = previousConfig.I18N,
};
}
}
private static Data MigrateV6ToV7(ILogger<SettingsManager> logger, string configData, JsonSerializerOptions jsonOptions)
{
logger.LogInformation("Migrating from v6 to v7...");
var root = JsonNode.Parse(configData, documentOptions: new JsonDocumentOptions
{
AllowTrailingCommas = jsonOptions.AllowTrailingCommas,
}) as JsonObject;
if (root is null)
{
logger.LogError("Failed to parse the v6 configuration. Using default values.");
return new();
}
root[nameof(Data.Version)] = nameof(Version.V7);
MigrateProfileSetting(root[nameof(Data.App)] as JsonObject, isAppSetting: true);
foreach (var sectionName in new[]
{
nameof(Data.Chat), nameof(Data.Agenda), nameof(Data.Coding), nameof(Data.EMail),
nameof(Data.ERI), nameof(Data.LegalCheck), nameof(Data.MyTasks), nameof(Data.SlideBuilder),
nameof(Data.BiasOfTheDay), nameof(Data.VisualBriefing),
})
MigrateProfileSetting(root[sectionName] as JsonObject, isAppSetting: false);
if (root[nameof(Data.DocumentAnalysis)]?[nameof(DataDocumentAnalysis.Policies)] is JsonArray policies)
{
foreach (var policy in policies.OfType<JsonObject>())
MigrateProfileSetting(policy, isAppSetting: false);
}
MigrateManagedSettingKeys(root[nameof(Data.ManagedLockedConfigurations)] as JsonObject);
MigrateManagedSettingKeys(root[nameof(Data.ManagedEditableDefaults)] as JsonObject);
MigrateManagedSnapshots(root[nameof(Data.ManagedUserValueSnapshots)] as JsonObject);
var migrated = root.Deserialize<Data>(jsonOptions);
if (migrated is not null)
return migrated;
logger.LogError("Failed to deserialize the migrated v7 configuration. Using default values.");
return new();
}
private static void MigrateProfileSetting(JsonObject? section, bool isAppSetting)
{
if (section is null || !section.Remove("PreselectedProfile", out var oldNode))
return;
var oldValue = oldNode?.GetValue<string>() ?? string.Empty;
JsonNode? newValue;
if (string.IsNullOrWhiteSpace(oldValue))
newValue = isAppSetting ? new JsonArray() : null;
else if (Guid.TryParse(oldValue, out var profileId) && profileId == Guid.Empty)
newValue = new JsonArray();
else
newValue = new JsonArray(JsonValue.Create(oldValue));
section["PreselectedProfileIds"] = newValue;
}
private static void MigrateManagedSettingKeys(JsonObject? settings)
{
if (settings is null)
return;
foreach (var oldKey in settings.Select(item => item.Key).Where(key => key.EndsWith(".PreselectedProfile", StringComparison.Ordinal)).ToList())
{
var value = settings[oldKey]?.DeepClone();
settings.Remove(oldKey);
settings[$"{oldKey}Ids"] = value;
}
}
private static void MigrateManagedSnapshots(JsonObject? snapshots)
{
if (snapshots is null)
return;
foreach (var oldKey in snapshots.Select(item => item.Key).Where(key => key.EndsWith(".PreselectedProfile", StringComparison.Ordinal)).ToList())
{
var snapshot = snapshots[oldKey]?.GetValue<string>() ?? "null";
string migratedSnapshot;
try
{
var oldValue = JsonSerializer.Deserialize<string?>(snapshot);
var isAppSetting = oldKey.Equals("DataApp.PreselectedProfile", StringComparison.Ordinal);
migratedSnapshot = string.IsNullOrWhiteSpace(oldValue)
? isAppSetting ? "[]" : "null"
: Guid.TryParse(oldValue, out var profileId) && profileId == Guid.Empty
? "[]"
: JsonSerializer.Serialize(new[] { oldValue });
}
catch (JsonException)
{
migratedSnapshot = oldKey.Equals("DataApp.PreselectedProfile", StringComparison.Ordinal) ? "[]" : "null";
}
snapshots.Remove(oldKey);
snapshots[$"{oldKey}Ids"] = migratedSnapshot;
}
}
}
+2 -1
View File
@@ -14,4 +14,5 @@ public enum Version
V4,
V5,
V6,
}
V7,
}