mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 13:53:47 +00:00
Multiple Profiles can now be selected
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,5 @@ public enum Version
|
||||
V4,
|
||||
V5,
|
||||
V6,
|
||||
}
|
||||
V7,
|
||||
}
|
||||
Reference in new issue
Block a user