mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-10 06:53:48 +00:00
Warn before exporting a chat template with local data sources
This commit is contained in:
3 files changed
+123
No files matched your search
@@ -132,6 +132,9 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!await this.ConfirmExportOfLocalDataSources(chatTemplate))
|
||||||
|
return;
|
||||||
|
|
||||||
await this.CopyChatTemplateLuaToClipboard(chatTemplate);
|
await this.CopyChatTemplateLuaToClipboard(chatTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +148,14 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
|
|
||||||
if (chatTemplate.FileAttachments.Count == 0)
|
if (chatTemplate.FileAttachments.Count == 0)
|
||||||
{
|
{
|
||||||
|
// That way asks about the local data sources itself, so we must not ask twice:
|
||||||
await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate);
|
await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await this.ConfirmExportOfLocalDataSources(chatTemplate))
|
||||||
|
return;
|
||||||
|
|
||||||
this.isPluginDirectoryDialogOpen = true;
|
this.isPluginDirectoryDialogOpen = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -164,6 +171,35 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks whether to export a template although it preselects data sources of this machine.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The export writes the preselected data source IDs unchanged, which is what makes a template
|
||||||
|
/// usable across an organization — but a local file or folder exists here and nowhere else, so
|
||||||
|
/// its ID points at nothing on the machine reading the plugin. Nothing breaks, the chat simply
|
||||||
|
/// starts without that source, and that is precisely why it has to be said beforehand: nobody
|
||||||
|
/// would notice it afterwards. Exporting anyway is a fair choice, because the rest of the
|
||||||
|
/// template is worth rolling out.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="chatTemplate">The chat template about to be exported.</param>
|
||||||
|
/// <returns>True when the export may go ahead.</returns>
|
||||||
|
private async Task<bool> ConfirmExportOfLocalDataSources(ChatTemplate chatTemplate)
|
||||||
|
{
|
||||||
|
var localDataSourceNames = ChatTemplate.GetPreselectedLocalDataSourceNames(chatTemplate, this.SettingsManager.ConfigurationData.DataSources);
|
||||||
|
if (localDataSourceNames.Count == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||||
|
{
|
||||||
|
{ x => x.Message, string.Format(T("This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?"), string.Join(", ", localDataSourceNames)) },
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Export Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
var dialogResult = await dialogReference.Result;
|
||||||
|
return dialogResult is { Canceled: false };
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
||||||
{
|
{
|
||||||
if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue))
|
if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue))
|
||||||
|
|||||||
@@ -145,6 +145,32 @@ public record ChatTemplate(
|
|||||||
return (templateOptions.CreateCopy(), launcherOptions is not null);
|
return (templateOptions.CreateCopy(), launcherOptions is not null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Names the preselected data sources which exist on this machine only.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Such a source is a sensible choice inside a chat and a dead end in an export: its ID travels
|
||||||
|
/// into the plugin unchanged, and on the machine which reads that plugin it points at nothing.
|
||||||
|
/// Only ERI sources describe something the whole organization can reach, which is why they are
|
||||||
|
/// also the only ones the app offers an export for.<br/><br/>
|
||||||
|
/// IDs which match no configured source at all are left out. Those are covered by the note the
|
||||||
|
/// export writes above the data source IDs anyway, and the name to warn about is missing.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="chatTemplate">The chat template about to be exported.</param>
|
||||||
|
/// <param name="configuredDataSources">The data sources configured on this machine.</param>
|
||||||
|
/// <returns>The names of the preselected local data sources, in the order they are configured in.</returns>
|
||||||
|
public static IReadOnlyList<string> GetPreselectedLocalDataSourceNames(ChatTemplate chatTemplate, IEnumerable<IDataSource> configuredDataSources)
|
||||||
|
{
|
||||||
|
if (chatTemplate.DataSourceOptions is not { PreselectedDataSourceIds.Count: > 0 } options)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var preselectedIds = options.PreselectedDataSourceIds.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
return configuredDataSources
|
||||||
|
.Where(source => source is IInternalDataSource && preselectedIds.Contains(source.Id))
|
||||||
|
.Select(source => source.Name)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template)
|
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template)
|
||||||
{
|
{
|
||||||
template = NO_CHAT_TEMPLATE;
|
template = NO_CHAT_TEMPLATE;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Settings.DataModel;
|
||||||
|
|
||||||
using Lua;
|
using Lua;
|
||||||
using Lua.Standard;
|
using Lua.Standard;
|
||||||
@@ -14,6 +15,8 @@ namespace AIStudio.Tests.Settings;
|
|||||||
/// template says nothing" and "this template says none" survives. Both end up as an empty
|
/// template says nothing" and "this template says none" survives. Both end up as an empty
|
||||||
/// selection in the chat on a fresh installation, so a mistake here stays invisible until somebody
|
/// selection in the chat on a fresh installation, so a mistake here stays invisible until somebody
|
||||||
/// sets a default tool for their chats -- and then quietly hands out a tool the template ruled out.
|
/// sets a default tool for their chats -- and then quietly hands out a tool the template ruled out.
|
||||||
|
/// The last tests cover what the export says out loud before it runs, for the same reason: a data
|
||||||
|
/// source which cannot be rolled out goes unnoticed on the machine reading the plugin.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class ChatTemplateConfigurationTests
|
public sealed class ChatTemplateConfigurationTests
|
||||||
@@ -183,6 +186,55 @@ public sealed class ChatTemplateConfigurationTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LocalDataSourcesOfATemplateAreNamedBeforeItIsExported()
|
||||||
|
{
|
||||||
|
var template = NewTemplate() with
|
||||||
|
{
|
||||||
|
DataSourceOptions = new()
|
||||||
|
{
|
||||||
|
DisableDataSources = false,
|
||||||
|
PreselectedDataSourceIds = ["11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var localNames = ChatTemplate.GetPreselectedLocalDataSourceNames(template, ConfiguredDataSources());
|
||||||
|
|
||||||
|
Assert.That(localNames, Is.EqualTo(new[] { "Meeting notes" }), "Only the local source can be named: its ID means nothing on the machine which reads the exported plugin, while the ERI source points at something the whole organization reaches.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ATemplateWithoutLocalDataSourcesIsExportedWithoutAQuestion()
|
||||||
|
{
|
||||||
|
var eriOnly = NewTemplate() with
|
||||||
|
{
|
||||||
|
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["22222222-2222-2222-2222-222222222222"] },
|
||||||
|
};
|
||||||
|
|
||||||
|
var agentic = NewTemplate() with
|
||||||
|
{
|
||||||
|
DataSourceOptions = new() { DisableDataSources = false, AutomaticDataSourceSelection = true },
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(eriOnly, ConfiguredDataSources()), Is.Empty);
|
||||||
|
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(agentic, ConfiguredDataSources()), Is.Empty, "An agent picks the sources per message, so this template names none to begin with.");
|
||||||
|
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(NewTemplate(), ConfiguredDataSources()), Is.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AnIdWhichMatchesNoDataSourceIsNotReportedAsALocalOne()
|
||||||
|
{
|
||||||
|
var template = NewTemplate() with
|
||||||
|
{
|
||||||
|
DataSourceOptions = new() { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] },
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.That(ChatTemplate.GetPreselectedLocalDataSourceNames(template, ConfiguredDataSources()), Is.Empty, "There is no name to warn about, and the note above the exported IDs already tells the admin to check them.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A template with the parts every export needs, and nothing said about tools or data sources.
|
/// A template with the parts every export needs, and nothing said about tools or data sources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -198,6 +250,15 @@ public sealed class ChatTemplateConfigurationTests
|
|||||||
AllowProfileUsage = true,
|
AllowProfileUsage = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One data source of each kind: a local one, which cannot be rolled out, and an ERI one, which can.
|
||||||
|
/// </summary>
|
||||||
|
private static IReadOnlyList<IDataSource> ConfiguredDataSources() =>
|
||||||
|
[
|
||||||
|
new DataSourceLocalFile { Id = "11111111-1111-1111-1111-111111111111", Name = "Meeting notes", Type = DataSourceType.LOCAL_FILE },
|
||||||
|
new DataSourceERI_V1 { Id = "22222222-2222-2222-2222-222222222222", Name = "Intranet", Type = DataSourceType.ERI_V1 },
|
||||||
|
];
|
||||||
|
|
||||||
private static async Task<ChatTemplate> ExportAndReadBackAsync(ChatTemplate template)
|
private static async Task<ChatTemplate> ExportAndReadBackAsync(ChatTemplate template)
|
||||||
{
|
{
|
||||||
Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue);
|
Assert.That(template.TryExportAsConfigurationSection(out var luaCode, out var issue), Is.True, issue);
|
||||||
|
|||||||
Reference in new issue
Block a user