mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 00:13:38 +00:00
First review
This commit is contained in:
parent
0e63f93696
commit
ad6b700978
@ -245,7 +245,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
this.selectedPolicy = this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.FirstOrDefault();
|
||||
if(this.selectedPolicy is null)
|
||||
{
|
||||
await this.AddInitialPolicy();
|
||||
await this.AddPolicy();
|
||||
this.selectedPolicy = this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.First();
|
||||
}
|
||||
|
||||
@ -484,32 +484,28 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task AddPolicy() => this.AddPolicy(null);
|
||||
|
||||
private async Task ImportPolicy()
|
||||
{
|
||||
if (this.ArePolicyControlsDisabled || !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
|
||||
return;
|
||||
var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "DOCUMENT_ANALYSIS_POLICIES", T("Import document analysis policy"));
|
||||
if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
|
||||
await this.AddPolicy(table);
|
||||
await this.AddImportedPolicy(table);
|
||||
}
|
||||
|
||||
private async Task AddPolicy(LuaTable? importedConfiguration)
|
||||
private async Task AddImportedPolicy(LuaTable importedConfiguration)
|
||||
{
|
||||
if (this.ArePolicyControlsDisabled)
|
||||
if (this.ArePolicyControlsDisabled || !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
|
||||
return;
|
||||
|
||||
if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
|
||||
return;
|
||||
|
||||
var parameters = new DialogParameters<DocumentAnalysisPolicyDialog>();
|
||||
if (importedConfiguration is not null)
|
||||
parameters.Add(x => x.ImportedConfiguration, importedConfiguration);
|
||||
var parameters = new DialogParameters<DocumentAnalysisPolicyDialog>
|
||||
{
|
||||
{ x => x.ImportedConfiguration, importedConfiguration },
|
||||
};
|
||||
var dialogReference = await this.DialogService.ShowAsync<DocumentAnalysisPolicyDialog>(T("Add policy"), parameters, DialogOptions.FULLSCREEN);
|
||||
var result = await dialogReference.Result;
|
||||
if (result is null || result.Canceled || result.Data is not DataDocumentAnalysisPolicy policy ||
|
||||
(importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES")))
|
||||
!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
|
||||
return;
|
||||
|
||||
var addedPolicy = policy with
|
||||
@ -521,7 +517,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
this.SelectedPolicyChanged(addedPolicy);
|
||||
}
|
||||
|
||||
private async Task AddInitialPolicy()
|
||||
private async Task AddPolicy()
|
||||
{
|
||||
if (this.ArePolicyControlsDisabled)
|
||||
return;
|
||||
|
||||
@ -17,10 +17,15 @@
|
||||
@for (var index = 0; index < this.attachmentsToRelink.Count; index++)
|
||||
{
|
||||
var currentIndex = index;
|
||||
<MudTextField T="string" Value="@this.attachmentsToRelink[index].ReplacementPath"
|
||||
ValueChanged="@(value => this.attachmentsToRelink[currentIndex] = (this.attachmentsToRelink[currentIndex].OriginalPath, value))"
|
||||
Label="@string.Format(T("Relink attachment: {0}"), this.attachmentsToRelink[index].OriginalPath)"
|
||||
HelperText="@T("Enter an absolute path to an existing local file before saving.")" Variant="Variant.Outlined" Class="mb-2" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudTextField T="string" Value="@this.attachmentsToRelink[index].ReplacementPath"
|
||||
ValueChanged="@(value => this.UpdateRelinkPath(currentIndex, value))"
|
||||
Label="@string.Format(T("Relink attachment: {0}"), this.attachmentsToRelink[index].OriginalPath)"
|
||||
HelperText="@T("Enter an absolute path to an existing local file, or remove this attachment.")" Variant="Variant.Outlined" />
|
||||
<MudTooltip Text="@T("Remove this attachment")">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(() => this.RemoveAttachmentToRelink(currentIndex))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
}
|
||||
}
|
||||
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||
|
||||
@ -200,33 +200,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
|
||||
private async Task ImportConfiguration(LuaTable table)
|
||||
{
|
||||
ConfigurationImportFields.ValidateExportId(table);
|
||||
ConfigurationImportFields.String(table, "Name");
|
||||
ConfigurationImportFields.String(table, "SystemPrompt");
|
||||
ConfigurationImportFields.String(table, "PredefinedUserPrompt", required: false);
|
||||
ConfigurationImportFields.Bool(table, "AllowProfileUsage");
|
||||
var messages = ConfigurationImportFields.Table(table, "ExampleConversation");
|
||||
for (var index = 1; index <= messages.ArrayLength; index++)
|
||||
{
|
||||
if (messages[index].Type is not LuaValueType.Table || !messages[index].TryRead<LuaTable>(out var message))
|
||||
throw new FormatException("An example conversation entry is not a table.");
|
||||
ConfigurationImportFields.Enum<ChatRole>(message, "Role");
|
||||
if (string.IsNullOrWhiteSpace(ConfigurationImportFields.String(message, "Content")))
|
||||
throw new FormatException("An example conversation message is empty.");
|
||||
}
|
||||
if (table.TryGetValue("ToolIds", out _))
|
||||
ConfigurationImportFields.Strings(table, "ToolIds");
|
||||
if (table.TryGetValue("DataSourceOptions", out _))
|
||||
{
|
||||
var options = ConfigurationImportFields.Table(table, "DataSourceOptions");
|
||||
ConfigurationImportFields.Bool(options, "DisableDataSources");
|
||||
ConfigurationImportFields.Bool(options, "AutomaticDataSourceSelection");
|
||||
ConfigurationImportFields.Bool(options, "AutomaticValidation");
|
||||
if (options.TryGetValue("PreselectedDataSourceIds", out _))
|
||||
ConfigurationImportFields.Strings(options, "PreselectedDataSourceIds");
|
||||
}
|
||||
ConfigurationSnippetImportValidation.Validate("CHAT_TEMPLATES", table);
|
||||
if (!ChatTemplate.TryParseChatTemplateTable(0, table, Guid.Empty, string.Empty, out var parsed) || parsed is not ChatTemplate template)
|
||||
throw new FormatException("The chat template fields are malformed.");
|
||||
throw new FormatException(T("The chat template fields are malformed."));
|
||||
var paths = ConfigurationImportFields.Strings(table, "FileAttachments");
|
||||
var validAttachments = new HashSet<FileAttachment>();
|
||||
var toRelink = new List<(string OriginalPath, string ReplacementPath)>();
|
||||
@ -260,8 +236,18 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
var availableToolIds = (await this.ToolRegistry.GetCatalogAsync(AIStudio.Tools.Components.CHAT))
|
||||
.Select(item => item.Definition.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var missingTools = this.selectedToolIds.Where(id => !availableToolIds.Contains(id)).ToList();
|
||||
var missing = missingSources.Select(id => $"data source {id}").Concat(missingTools.Select(id => $"tool {id}")).ToList();
|
||||
return missing.Count == 0 ? string.Empty : $"Unavailable references: {string.Join(", ", missing)}. Review the selection before saving.";
|
||||
var missing = missingSources.Select(ConfigurationImportFields.MissingDataSourceReference)
|
||||
.Concat(missingTools.Select(ConfigurationImportFields.MissingToolReference)).ToList();
|
||||
return ConfigurationImportFields.UnavailableReferencesIssue(missing);
|
||||
}
|
||||
|
||||
private void UpdateRelinkPath(int index, string path) => this.attachmentsToRelink[index] = (this.attachmentsToRelink[index].OriginalPath, path);
|
||||
|
||||
private void RemoveAttachmentToRelink(int index)
|
||||
{
|
||||
this.attachmentsToRelink.RemoveAt(index);
|
||||
if (this.attachmentsToRelink.Count == 0)
|
||||
this.relinkIssue = string.Empty;
|
||||
}
|
||||
|
||||
private void SetSelectedToolIds(HashSet<string> toolIds) => this.selectedToolIds = toolIds;
|
||||
@ -365,15 +351,16 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
if (this.IsReadOnly)
|
||||
return;
|
||||
|
||||
// Only check the relinked attachments here. They are added right before closing, so that a
|
||||
// failed save does not leave a path behind which the user changes afterward:
|
||||
this.relinkIssue = string.Empty;
|
||||
foreach (var (originalPath, replacementPath) in this.attachmentsToRelink)
|
||||
{
|
||||
if (!ConfigurationImportFields.IsExistingLocalFile(replacementPath))
|
||||
{
|
||||
this.relinkIssue = string.Format(T("Relink the missing attachment '{0}' to an existing local file before saving."), originalPath);
|
||||
this.relinkIssue = string.Format(T("Relink the missing attachment '{0}' to an existing local file or remove it before saving."), originalPath);
|
||||
return;
|
||||
}
|
||||
this.fileAttachments.Add(FileAttachment.FromPath(replacementPath));
|
||||
}
|
||||
|
||||
await this.form.Validate();
|
||||
@ -386,6 +373,10 @@ public partial class ChatTemplateDialog : MSGComponentBase
|
||||
if (this.isInlineEditOnGoing)
|
||||
return;
|
||||
|
||||
foreach (var (_, replacementPath) in this.attachmentsToRelink)
|
||||
this.fileAttachments.Add(FileAttachment.FromPath(replacementPath));
|
||||
this.attachmentsToRelink.Clear();
|
||||
|
||||
// Use the data model to store the chat template.
|
||||
// We just return this data to the parent component:
|
||||
var addedChatTemplateSettings = this.CreateChatTemplateSettings();
|
||||
|
||||
@ -196,22 +196,14 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
|
||||
private Task ImportConfiguration(LuaTable table)
|
||||
{
|
||||
ConfigurationImportFields.ValidateExportId(table);
|
||||
if (ConfigurationImportFields.String(table, "Type") != "ERI_V1")
|
||||
throw new FormatException("This data source is not an ERI v1 data source.");
|
||||
ConfigurationSnippetImportValidation.Validate("DATA_SOURCES", table);
|
||||
var name = ConfigurationImportFields.String(table, "Name");
|
||||
var hostname = ConfigurationImportFields.String(table, "Hostname");
|
||||
var port = ConfigurationImportFields.Int(table, "Port");
|
||||
if (port is < 1 or > 65535)
|
||||
throw new FormatException("The 'Port' field must be between 1 and 65535.");
|
||||
var authMethod = ConfigurationImportFields.Enum<AuthMethod>(table, "AuthMethod");
|
||||
if (authMethod is AuthMethod.KERBEROS)
|
||||
throw new FormatException("Kerberos data sources cannot be imported from configuration snippets.");
|
||||
var securityPolicy = ConfigurationImportFields.Enum<DataSourceSecurity>(table, "SecurityPolicy");
|
||||
var retrievalId = ConfigurationImportFields.String(table, "SelectedRetrievalId");
|
||||
var maxMatches = ConfigurationImportFields.Int(table, "MaxMatches", 10);
|
||||
if (maxMatches is < 1 or > ushort.MaxValue)
|
||||
throw new FormatException("The 'MaxMatches' field is outside the allowed range.");
|
||||
var secretName = authMethod switch
|
||||
{
|
||||
AuthMethod.TOKEN => "Token",
|
||||
|
||||
@ -75,13 +75,13 @@ public partial class DocumentAnalysisPolicyDialog : MSGComponentBase
|
||||
|
||||
var missing = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(this.providerId) && this.SettingsManager.GetAllProviders().All(provider => provider.Id != this.providerId))
|
||||
missing.Add($"provider {this.providerId}");
|
||||
missing.Add(ConfigurationImportFields.MissingProviderReference(this.providerId));
|
||||
if (!string.IsNullOrWhiteSpace(this.profileId) && this.profileId != Profile.NO_PROFILE.Id && this.SettingsManager.ConfigurationData.Profiles.All(profile => profile.Id != this.profileId))
|
||||
missing.Add($"profile {this.profileId}");
|
||||
missing.Add(ConfigurationImportFields.MissingProfileReference(this.profileId));
|
||||
var availableToolIds = (await this.ToolRegistry.GetCatalogAsync(AIStudio.Tools.Components.DOCUMENT_ANALYSIS_ASSISTANT))
|
||||
.Select(item => item.Definition.Id).ToHashSet(StringComparer.Ordinal);
|
||||
missing.AddRange(this.allowedToolIds.Where(id => !availableToolIds.Contains(id)).Select(id => $"tool {id}"));
|
||||
this.referenceIssue = missing.Count == 0 ? string.Empty : $"Unavailable references: {string.Join(", ", missing)}. Review the selections before saving.";
|
||||
missing.AddRange(this.allowedToolIds.Where(id => !availableToolIds.Contains(id)).Select(ConfigurationImportFields.MissingToolReference));
|
||||
this.referenceIssue = ConfigurationImportFields.UnavailableReferencesIssue(missing);
|
||||
this.form.ResetValidation();
|
||||
}
|
||||
|
||||
|
||||
@ -6,10 +6,12 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static class ConfigurationImportFields
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationImportFields).Namespace, nameof(ConfigurationImportFields));
|
||||
|
||||
public static void ValidateExportId(LuaTable table)
|
||||
{
|
||||
if (!Guid.TryParse(String(table, "Id"), out _))
|
||||
throw new FormatException("The exported item has an invalid ID.");
|
||||
throw new FormatException(TB("The exported item has an invalid ID."));
|
||||
}
|
||||
|
||||
public static string String(LuaTable table, string name, bool required = true)
|
||||
@ -18,18 +20,18 @@ public static class ConfigurationImportFields
|
||||
{
|
||||
if (value.Type is LuaValueType.String && value.TryRead<string>(out var text))
|
||||
return text;
|
||||
throw new FormatException($"The '{name}' field must be a string.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field must be a string."), name));
|
||||
}
|
||||
if (!required)
|
||||
return string.Empty;
|
||||
throw new FormatException($"The '{name}' field must be a string.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field must be a string."), name));
|
||||
}
|
||||
|
||||
public static LuaTable Table(LuaTable table, string name)
|
||||
{
|
||||
if (table.TryGetValue(name, out var value) && value.Type is LuaValueType.Table && value.TryRead<LuaTable>(out var nested))
|
||||
return nested;
|
||||
throw new FormatException($"The '{name}' field must be a table.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field must be a table."), name));
|
||||
}
|
||||
|
||||
public static T Enum<T>(LuaTable table, string name) where T : struct, Enum
|
||||
@ -37,7 +39,7 @@ public static class ConfigurationImportFields
|
||||
var text = String(table, name);
|
||||
if (System.Enum.TryParse<T>(text, true, out var result) && System.Enum.IsDefined(result))
|
||||
return result;
|
||||
throw new FormatException($"The '{name}' field has an unknown value.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field has an unknown value."), name));
|
||||
}
|
||||
|
||||
public static bool Bool(LuaTable table, string name, bool fallback = false)
|
||||
@ -46,7 +48,7 @@ public static class ConfigurationImportFields
|
||||
return fallback;
|
||||
if (value.Type is LuaValueType.Boolean && value.TryRead<bool>(out var result))
|
||||
return result;
|
||||
throw new FormatException($"The '{name}' field must be true or false.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field must be true or false."), name));
|
||||
}
|
||||
|
||||
public static int Int(LuaTable table, string name, int fallback = 0)
|
||||
@ -55,7 +57,7 @@ public static class ConfigurationImportFields
|
||||
return fallback;
|
||||
if (value.Type is LuaValueType.Number && value.TryRead<double>(out var number) && number >= int.MinValue && number <= int.MaxValue && number == Math.Truncate(number))
|
||||
return (int)number;
|
||||
throw new FormatException($"The '{name}' field must be a whole number.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field must be a whole number."), name));
|
||||
}
|
||||
|
||||
public static List<string> Strings(LuaTable table, string name)
|
||||
@ -65,7 +67,7 @@ public static class ConfigurationImportFields
|
||||
for (var i = 1; i <= nested.ArrayLength; i++)
|
||||
{
|
||||
if (nested[i].Type is not LuaValueType.String || !nested[i].TryRead<string>(out var text) || string.IsNullOrWhiteSpace(text))
|
||||
throw new FormatException($"The '{name}' field contains an invalid entry.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field contains an invalid entry."), name));
|
||||
result.Add(text);
|
||||
}
|
||||
return result;
|
||||
@ -73,6 +75,19 @@ public static class ConfigurationImportFields
|
||||
|
||||
public static bool IsExistingLocalFile(string path) => Path.IsPathFullyQualified(path) && File.Exists(path);
|
||||
|
||||
/// <summary>Lists the given references as a warning, or returns an empty text when nothing is missing.</summary>
|
||||
public static string UnavailableReferencesIssue(IReadOnlyCollection<string> missing) => missing.Count == 0
|
||||
? string.Empty
|
||||
: string.Format(TB("Unavailable references: {0}. Review the selections before saving."), string.Join(", ", missing));
|
||||
|
||||
public static string MissingProviderReference(string id) => string.Format(TB("provider {0}"), id);
|
||||
|
||||
public static string MissingProfileReference(string id) => string.Format(TB("profile {0}"), id);
|
||||
|
||||
public static string MissingToolReference(string id) => string.Format(TB("tool {0}"), id);
|
||||
|
||||
public static string MissingDataSourceReference(string id) => string.Format(TB("data source {0}"), id);
|
||||
|
||||
public static string Credential(LuaTable table, string name, out string issue, EnterpriseEncryption? decryptionService = null)
|
||||
{
|
||||
issue = string.Empty;
|
||||
@ -80,11 +95,11 @@ public static class ConfigurationImportFields
|
||||
if (string.IsNullOrEmpty(encrypted))
|
||||
return string.Empty;
|
||||
if (!EnterpriseEncryption.IsEncrypted(encrypted))
|
||||
throw new FormatException($"The '{name}' field must contain an ENC:v1 credential.");
|
||||
throw new FormatException(string.Format(TB("The '{0}' field must contain an ENC:v1 credential."), name));
|
||||
var encryption = decryptionService ?? PluginFactory.EnterpriseEncryption;
|
||||
if (encryption?.IsAvailable == true && encryption.TryDecrypt(encrypted, out var decrypted))
|
||||
return decrypted;
|
||||
issue = "The embedded credential could not be decrypted on this device. Enter your own credential before saving.";
|
||||
issue = TB("The embedded credential could not be decrypted on this device. Enter your own credential before saving.");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,8 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
/// <summary>Checks the fields used by the creation forms before leaving the paste dialog.</summary>
|
||||
public static class ConfigurationSnippetImportValidation
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationSnippetImportValidation).Namespace, nameof(ConfigurationSnippetImportValidation));
|
||||
|
||||
public static void Validate(string section, LuaTable table)
|
||||
{
|
||||
ConfigurationImportFields.ValidateExportId(table);
|
||||
@ -47,7 +49,7 @@ public static class ConfigurationSnippetImportValidation
|
||||
ConfigurationImportFields.Bool(table, "HidePolicyDefinition");
|
||||
break;
|
||||
default:
|
||||
throw new FormatException("This configuration section cannot be imported here.");
|
||||
throw new FormatException(TB("This configuration section cannot be imported here."));
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,10 +86,10 @@ public static class ConfigurationSnippetImportValidation
|
||||
for (var index = 1; index <= messages.ArrayLength; index++)
|
||||
{
|
||||
if (messages[index].Type is not LuaValueType.Table || !messages[index].TryRead<LuaTable>(out var message))
|
||||
throw new FormatException("An example conversation entry is not a table.");
|
||||
throw new FormatException(TB("An example conversation entry is not a table."));
|
||||
ConfigurationImportFields.Enum<ChatRole>(message, "Role");
|
||||
if (string.IsNullOrWhiteSpace(ConfigurationImportFields.String(message, "Content")))
|
||||
throw new FormatException("An example conversation message is empty.");
|
||||
throw new FormatException(TB("An example conversation message is empty."));
|
||||
}
|
||||
if (table.TryGetValue("ToolIds", out _))
|
||||
ConfigurationImportFields.Strings(table, "ToolIds");
|
||||
@ -101,27 +103,27 @@ public static class ConfigurationSnippetImportValidation
|
||||
ConfigurationImportFields.Strings(options, "PreselectedDataSourceIds");
|
||||
}
|
||||
if (!ChatTemplate.TryParseChatTemplateTable(0, table, Guid.Empty, string.Empty, out _))
|
||||
throw new FormatException("The chat template fields are malformed.");
|
||||
throw new FormatException(TB("The chat template fields are malformed."));
|
||||
ConfigurationImportFields.Strings(table, "FileAttachments");
|
||||
}
|
||||
|
||||
private static void ValidateERIDataSource(LuaTable table)
|
||||
{
|
||||
if (ConfigurationImportFields.String(table, "Type") != "ERI_V1")
|
||||
throw new FormatException("This data source is not an ERI v1 data source.");
|
||||
throw new FormatException(TB("This data source is not an ERI v1 data source."));
|
||||
ConfigurationImportFields.String(table, "Name");
|
||||
ConfigurationImportFields.String(table, "Hostname");
|
||||
var port = ConfigurationImportFields.Int(table, "Port");
|
||||
if (port is < 1 or > 65535)
|
||||
throw new FormatException("The 'Port' field must be between 1 and 65535.");
|
||||
throw new FormatException(TB("The 'Port' field must be between 1 and 65535."));
|
||||
var authMethod = ConfigurationImportFields.Enum<AuthMethod>(table, "AuthMethod");
|
||||
if (authMethod is AuthMethod.KERBEROS)
|
||||
throw new FormatException("Kerberos data sources cannot be imported from configuration snippets.");
|
||||
throw new FormatException(TB("Kerberos data sources cannot be imported from configuration snippets."));
|
||||
ConfigurationImportFields.Enum<DataSourceSecurity>(table, "SecurityPolicy");
|
||||
ConfigurationImportFields.String(table, "SelectedRetrievalId");
|
||||
var maxMatches = ConfigurationImportFields.Int(table, "MaxMatches", 10);
|
||||
if (maxMatches is < 1 or > ushort.MaxValue)
|
||||
throw new FormatException("The 'MaxMatches' field is outside the allowed range.");
|
||||
throw new FormatException(TB("The 'MaxMatches' field is outside the allowed range."));
|
||||
var secretName = authMethod switch
|
||||
{
|
||||
AuthMethod.TOKEN => "Token",
|
||||
|
||||
@ -13,13 +13,15 @@ public sealed class ConfigurationSnippetParser
|
||||
|
||||
private ConfigurationSnippetParser(string source) => this.source = source;
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationSnippetParser).Namespace, nameof(ConfigurationSnippetParser));
|
||||
|
||||
public static bool TryParse(string source, string expectedSection, out LuaTable table, out string issue)
|
||||
{
|
||||
table = new LuaTable();
|
||||
issue = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(source) || source.Length > 1_000_000)
|
||||
{
|
||||
issue = "Paste one exported configuration snippet (up to 1 MB).";
|
||||
issue = TB("Paste one exported configuration snippet (up to 1 MB).");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -29,13 +31,13 @@ public sealed class ConfigurationSnippetParser
|
||||
parser.ExpectWord("CONFIG");
|
||||
var section = parser.ReadBracketedString();
|
||||
if (section != expectedSection)
|
||||
throw new FormatException($"This is a {section} snippet. Paste a {expectedSection} snippet here.");
|
||||
throw new FormatException(string.Format(TB("This is a {0} snippet. Paste a {1} snippet here."), section, expectedSection));
|
||||
|
||||
parser.Expect('[');
|
||||
parser.Expect('#');
|
||||
parser.ExpectWord("CONFIG");
|
||||
if (parser.ReadBracketedString() != expectedSection)
|
||||
throw new FormatException("The configuration section names do not match.");
|
||||
throw new FormatException(TB("The configuration section names do not match."));
|
||||
parser.Expect('+');
|
||||
parser.Expect('1');
|
||||
parser.Expect(']');
|
||||
@ -43,7 +45,7 @@ public sealed class ConfigurationSnippetParser
|
||||
table = parser.ReadTable(0);
|
||||
parser.SkipTrivia();
|
||||
if (parser.position != source.Length)
|
||||
throw new FormatException("The snippet must contain exactly one table assignment and no executable code.");
|
||||
throw new FormatException(TB("The snippet must contain exactly one table assignment and no executable code."));
|
||||
return true;
|
||||
}
|
||||
catch (FormatException exception)
|
||||
@ -56,7 +58,7 @@ public sealed class ConfigurationSnippetParser
|
||||
private LuaTable ReadTable(int depth)
|
||||
{
|
||||
if (depth > 32)
|
||||
throw new FormatException("The snippet contains too many nested tables.");
|
||||
throw new FormatException(TB("The snippet contains too many nested tables."));
|
||||
this.Expect('{');
|
||||
var table = new LuaTable();
|
||||
var arrayIndex = 1;
|
||||
@ -67,13 +69,15 @@ public sealed class ConfigurationSnippetParser
|
||||
if (this.Take('}'))
|
||||
return table;
|
||||
|
||||
if (this.Take('['))
|
||||
// "[[" or "[=" opens a long string, which is an array value rather than a bracketed key:
|
||||
if (this.Peek() == '[' && !this.IsLongStringStart())
|
||||
{
|
||||
this.position++;
|
||||
var key = this.ReadString();
|
||||
this.Expect(']');
|
||||
this.Expect('=');
|
||||
if (!names.Add(key))
|
||||
throw new FormatException($"The field '{key}' occurs more than once.");
|
||||
throw new FormatException(string.Format(TB("The field '{0}' occurs more than once."), key));
|
||||
table[key] = this.ReadValue(depth + 1);
|
||||
}
|
||||
else
|
||||
@ -83,7 +87,7 @@ public sealed class ConfigurationSnippetParser
|
||||
if (this.Take('}'))
|
||||
return table;
|
||||
if (!this.Take(',') && !this.Take(';'))
|
||||
throw new FormatException($"Expected a comma or closing brace at character {this.position + 1}.");
|
||||
throw new FormatException(string.Format(TB("Expected a comma or closing brace at character {0}."), this.position + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,9 +118,11 @@ public sealed class ConfigurationSnippetParser
|
||||
}
|
||||
if (this.position > start && double.TryParse(this.source[start..this.position], NumberStyles.Float, CultureInfo.InvariantCulture, out var number) && double.IsFinite(number))
|
||||
return number;
|
||||
throw new FormatException($"Only literal values are allowed at character {start + 1}; executable Lua is not accepted.");
|
||||
throw new FormatException(string.Format(TB("Only literal values are allowed at character {0}; executable Lua is not accepted."), start + 1));
|
||||
}
|
||||
|
||||
private bool IsLongStringStart() => this.position + 1 < this.source.Length && this.source[this.position + 1] is '[' or '=';
|
||||
|
||||
private string ReadBracketedString()
|
||||
{
|
||||
this.Expect('[');
|
||||
@ -136,7 +142,9 @@ public sealed class ConfigurationSnippetParser
|
||||
while (this.Peek() == '=')
|
||||
this.position++;
|
||||
var equals = this.source[equalsStart..this.position];
|
||||
this.Expect('[');
|
||||
if (this.Peek() != '[')
|
||||
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), '[', this.position + 1));
|
||||
this.position++;
|
||||
if (this.Peek() is '\r' or '\n')
|
||||
{
|
||||
if (this.Take('\r'))
|
||||
@ -146,13 +154,13 @@ public sealed class ConfigurationSnippetParser
|
||||
}
|
||||
var end = this.source.IndexOf("]" + equals + "]", this.position, StringComparison.Ordinal);
|
||||
if (end < 0)
|
||||
throw new FormatException("Unterminated long string.");
|
||||
throw new FormatException(TB("Unterminated long string."));
|
||||
var value = this.source[this.position..end];
|
||||
this.position = end + equals.Length + 2;
|
||||
return value;
|
||||
}
|
||||
if (quote is not ('"' or '\''))
|
||||
throw new FormatException($"Expected a quoted string at character {this.position + 1}.");
|
||||
throw new FormatException(string.Format(TB("Expected a quoted string at character {0}."), this.position + 1));
|
||||
this.position++;
|
||||
var builder = new StringBuilder();
|
||||
while (this.position < this.source.Length)
|
||||
@ -161,7 +169,7 @@ public sealed class ConfigurationSnippetParser
|
||||
if (c == quote)
|
||||
return builder.ToString();
|
||||
if (c is '\r' or '\n')
|
||||
throw new FormatException("A quoted string contains an unescaped newline.");
|
||||
throw new FormatException(TB("A quoted string contains an unescaped newline."));
|
||||
if (c != '\\')
|
||||
{
|
||||
builder.Append(c);
|
||||
@ -174,10 +182,10 @@ public sealed class ConfigurationSnippetParser
|
||||
{
|
||||
'n' => '\n', 'r' => '\r', 't' => '\t', 'a' => '\a', 'b' => '\b', 'f' => '\f', 'v' => '\v',
|
||||
'\\' => '\\', '"' => '"', '\'' => '\'',
|
||||
_ => throw new FormatException($"Unsupported string escape \\{c}."),
|
||||
_ => throw new FormatException(string.Format(TB("Unsupported string escape sequence: {0}"), "\\" + c)),
|
||||
});
|
||||
}
|
||||
throw new FormatException("Unterminated quoted string.");
|
||||
throw new FormatException(TB("Unterminated quoted string."));
|
||||
}
|
||||
|
||||
private void SkipTrivia()
|
||||
@ -214,7 +222,7 @@ public sealed class ConfigurationSnippetParser
|
||||
private void Expect(char c)
|
||||
{
|
||||
if (!this.Take(c))
|
||||
throw new FormatException($"Expected '{c}' at character {this.position + 1}.");
|
||||
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), c, this.position + 1));
|
||||
}
|
||||
|
||||
private bool TakeWord(string word)
|
||||
@ -230,6 +238,6 @@ public sealed class ConfigurationSnippetParser
|
||||
private void ExpectWord(string word)
|
||||
{
|
||||
if (!this.TakeWord(word))
|
||||
throw new FormatException($"Expected '{word}' at character {this.position + 1}.");
|
||||
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), word, this.position + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# v26.9.1, build 256 (2026-09-xx xx:xx UTC)
|
||||
- Added labeled import buttons beside Add for your providers, profiles, chat templates, ERI v1 data sources, and document analysis policies. Paste an exported configuration snippet, review the filled form, and save it as your own item.
|
||||
- Added controls for organizations to allow or lock configuration snippet imports by item type. A locked Import button stays visible, and plugin archive imports keep their own controls.
|
||||
- Added a way to relink missing file attachments when you create a chat template from an exported configuration.
|
||||
- Added a way to relink or remove missing file attachments when you create a chat template from an exported configuration.
|
||||
- Added a way to copy an entire chat, either with the button in the chat toolbar or next to the chat in the chat list. The copy opens right away so you can continue in it, while the original conversation stays exactly as it was. Many thanks to Peer Hogeterp (`peerschuett`) and Jens Erler (`j-erler`) for this feature.
|
||||
- Added a way to roll a chat back to an earlier AI response. The response you pick stays, and every message after it is removed permanently, together with the attachments of those messages.
|
||||
- Added a way to save a single code block of an answer. When an answer holds a web page, a LaTeX document, or a Markdown text, the export menu now offers that block as a file of its own.
|
||||
|
||||
@ -623,7 +623,7 @@ Handing a whole plugin to a colleague is a different thing: that is the **Share*
|
||||
|
||||
Each parent screen has a button naming the item to import beside **Add** for profiles, LLM providers, embedding providers, transcription providers, chat templates, and document analysis policies. The data source screen has **Import ERI v1 Data Source** beside **Add Data Source**. Copy an exported snippet from the item's **Export configuration** control, paste one snippet of the matching type into the import dialog, and choose its item-specific import button. AI Studio reads the table as data; it does not run Lua code. The existing creation form then opens with its fields filled in. Review them and choose **Add** to create the item. Canceling either dialog leaves nothing saved. Tool Settings snippets cannot be imported this way.
|
||||
|
||||
The imported item receives a new ID and the next local number. It belongs to you, even if the snippet came from an organization configuration. Existing provider, profile, tool, and data source references are kept; unavailable references are flagged so you can review them. Chat template attachments must point to existing local files. Relink any missing or relative paths in the creation dialog before saving.
|
||||
The imported item receives a new ID and the next local number. It belongs to you, even if the snippet came from an organization configuration. Existing provider, profile, tool, and data source references are kept; unavailable references are flagged so you can review them. Chat template attachments must point to existing local files. Relink or remove any missing or relative paths in the creation dialog before saving.
|
||||
|
||||
If an exported provider or ERI source contains an `ENC:v1` credential, AI Studio can fill it only on a device with the matching enterprise encryption secret. Otherwise, enter your own key, token, or password. A credential is stored in the local operating system secret store only when you save the item. Exporting still requires **Show administration settings**.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user