Added prompt overrides for testing the BeyondFacts Lab (#1181)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-10 08:57:36 +01:00
1 parent 9f2280bed7
commit 6d0052ecdf
21 files changed
+1035 -13

No files matched your search

@@ -128,6 +128,8 @@
<MudGrid Spacing="3"> <MudGrid Spacing="3">
<MudItem xs="12" lg="8"> <MudItem xs="12" lg="8">
<div @key="@this.activeStep" class="px-6"> <div @key="@this.activeStep" class="px-6">
@* It checks the test folder anew for every step, as the key of the step recreates it: *@
<BflPromptOverrideNotice Class="mt-2 mb-2"/>
<MudText Typo="Typo.h5" Class="mt-2">@this.activeStep.Title</MudText> <MudText Typo="Typo.h5" Class="mt-2">@this.activeStep.Title</MudText>
<MudText Typo="Typo.body2" Class="mb-4">@this.activeStep.Description</MudText> <MudText Typo="Typo.body2" Class="mb-4">@this.activeStep.Description</MudText>
@@ -73,6 +73,11 @@ public sealed partial class BeyondFactsLabStore(ILogger<BeyondFactsLabStore> log
throw new InvalidOperationException("The AI Studio data directory is not initialized."), throw new InvalidOperationException("The AI Studio data directory is not initialized."),
ROOT_DIRECTORY_NAME); ROOT_DIRECTORY_NAME);
/// <summary>
/// Gets the folder whose files replace the prompts of the lab while the person allows it, see BflPromptOverrides.
/// </summary>
public string PromptOverridesDirectory => Path.Combine(this.RootDirectory, BflPromptOverrides.DIRECTORY_NAME);
/// <summary> /// <summary>
/// Writes a text file so that readers see either the old or the new content, never a part of it. /// Writes a text file so that readers see either the old or the new content, never a part of it.
/// </summary> /// </summary>
@@ -58,6 +58,12 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
/// </summary> /// </summary>
private readonly Lock runningLock = new(); private readonly Lock runningLock = new();
/// <summary>
/// Gets the folder whose files replace the prompts of the lab, or null while the person does not allow it. The
/// setting counts anew for every request, so that switching it on or off shows in the next answer.
/// </summary>
private string? PromptOverridesDirectory => BflPromptOverrides.ActiveDirectory(settingsManager.ConfigurationData.BeyondFactsLab, store.PromptOverridesDirectory);
/// <summary> /// <summary>
/// Happens whenever a chat changed: a message, a chunk of an answer, or the end of a request. /// Happens whenever a chat changed: a message, a chunk of an answer, or the end of a request.
/// </summary> /// </summary>
@@ -144,6 +150,12 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
{ {
requestMaterial = await this.ComposeRequestAsync(location, provider); requestMaterial = await this.ComposeRequestAsync(location, provider);
} }
catch (BflPromptOverrideException exception)
{
logger.LogWarning(exception, "A prompt override of the BeyondFacts Lab has a placeholder which the lab does not fill.");
await messageBus.SendError(new(Icons.Material.Filled.ErrorOutline, string.Format(TA("The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again."),exception.FileName, exception.Placeholder)));
return;
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or FormatException) catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or FormatException)
{ {
logger.LogError(exception, "The BeyondFacts Lab could not put together the system prompt of the chat '{Location}'.", location.Key); logger.LogError(exception, "The BeyondFacts Lab could not put together the system prompt of the chat '{Location}'.", location.Key);
@@ -285,6 +297,14 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
/// <exception cref="FormatException">A prompt has a placeholder without a value.</exception> /// <exception cref="FormatException">A prompt has a placeholder without a value.</exception>
public async Task<string> ComposeSystemPromptAsync(BflThreadLocation location, AIStudio.Settings.Provider provider) => (await this.ComposeRequestAsync(location, provider)).SystemPrompt; public async Task<string> ComposeSystemPromptAsync(BflThreadLocation location, AIStudio.Settings.Provider provider) => (await this.ComposeRequestAsync(location, provider)).SystemPrompt;
/// <summary>
/// Checks whether files in the test folder replace prompts of the lab right now, so that the lab can tell the
/// person.
/// </summary>
/// <param name="token">The cancellation token.</param>
/// <returns>True while the setting allows prompt overrides and a file replaces a prompt with another text.</returns>
public async Task<bool> UsesPromptOverridesAsync(CancellationToken token = default) => this.PromptOverridesDirectory is { } directory && (await BflPromptOverrides.FindAsync(directory, token)).Count > 0;
/// <summary> /// <summary>
/// Puts together what a request of a chat carries beside the conversation: the system prompt, and the images of /// Puts together what a request of a chat carries beside the conversation: the system prompt, and the images of
/// the material the model may see. /// the material the model may see.
@@ -302,13 +322,13 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
{ {
case BflStepThreadLocation stepLocation: case BflStepThreadLocation stepLocation:
var forms = await store.LoadFormsAsync(stepLocation.ProjectId); var forms = await store.LoadFormsAsync(stepLocation.ProjectId);
var parts = await BflPromptLibrary.LoadPartsAsync(stepLocation.Step, addressForm); var parts = await BflPromptLibrary.LoadPartsAsync(stepLocation.Step, addressForm, this.PromptOverridesDirectory);
var shares = await this.ShareMaterialsAsync(stepLocation, parts, forms, language, provider); var shares = await this.ShareMaterialsAsync(stepLocation, parts, forms, language, provider);
var materials = await this.ReadMaterialsAsync(stepLocation.ProjectId, shares); var materials = await this.ReadMaterialsAsync(stepLocation.ProjectId, shares);
return new(BflPromptComposer.Compose(parts, stepLocation.Step, forms, materials, language), this.ImagesOf(stepLocation.ProjectId, shares)); return new(BflPromptComposer.Compose(parts, stepLocation.Step, forms, materials, language), this.ImagesOf(stepLocation.ProjectId, shares));
case BflOnboardingThreadLocation: case BflOnboardingThreadLocation:
return new(BflPromptComposer.ComposeOnboarding(await BflPromptLibrary.LoadOnboardingPartsAsync(addressForm), language), []); return new(BflPromptComposer.ComposeOnboarding(await BflPromptLibrary.LoadOnboardingPartsAsync(addressForm, this.PromptOverridesDirectory), language), []);
default: default:
throw new ArgumentOutOfRangeException(nameof(location), location, "The lab has no prompt for this chat."); throw new ArgumentOutOfRangeException(nameof(location), location, "The lab has no prompt for this chat.");
@@ -406,7 +426,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
{ {
var language = (await settingsManager.GetActiveLanguagePlugin()).LangName; var language = (await settingsManager.GetActiveLanguagePlugin()).LangName;
var forms = await store.LoadFormsAsync(location.ProjectId); var forms = await store.LoadFormsAsync(location.ProjectId);
var parts = await BflPromptLibrary.LoadPartsAsync(location.Step, Tools.PluginSystem.I18N.I.AddressForm); var parts = await BflPromptLibrary.LoadPartsAsync(location.Step, Tools.PluginSystem.I18N.I.AddressForm, this.PromptOverridesDirectory);
return await this.ShareMaterialsAsync(location, parts, forms, language, provider); return await this.ShareMaterialsAsync(location, parts, forms, language, provider);
} }
@@ -1,4 +1,5 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<BflPromptOverrideNotice Class="mb-3"/>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidateProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidateProvider"/>
<BflChat Location="@BflOnboardingThreadLocation.INSTANCE" Provider="@this.providerSettings" ProviderIssue="@this.ValidateProvider(this.providerSettings)" ShowHeading="@false"/> <BflChat Location="@BflOnboardingThreadLocation.INSTANCE" Provider="@this.providerSettings" ProviderIssue="@this.ValidateProvider(this.providerSettings)" ShowHeading="@false"/>
@@ -1,3 +1,5 @@
using System.Collections.Frozen;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BeyondFactsLab; namespace AIStudio.Assistants.BeyondFactsLab;
@@ -8,6 +10,7 @@ namespace AIStudio.Assistants.BeyondFactsLab;
/// <remarks> /// <remarks>
/// The files are German Markdown below Assistants/BeyondFactsLab, embedded into the app. A development build reads /// The files are German Markdown below Assistants/BeyondFactsLab, embedded into the app. A development build reads
/// them from the disk, so that a changed prompt shows in the next request without building again; see AppResources. /// them from the disk, so that a changed prompt shows in the next request without building again; see AppResources.
/// While the person allows it, files in the folder for prompt overrides replace them, see BflPromptOverrides.
/// </remarks> /// </remarks>
public static class BflPromptLibrary public static class BflPromptLibrary
{ {
@@ -42,6 +45,30 @@ public static class BflPromptLibrary
/// </summary> /// </summary>
private const string ADDRESS_INFORMAL_PATH = FOLDER + "/Prompts/snippets/address-informal.md"; private const string ADDRESS_INFORMAL_PATH = FOLDER + "/Prompts/snippets/address-informal.md";
/// <summary>
/// The placeholders BflPromptComposer fills in the core.
/// </summary>
private static readonly FrozenSet<string> CORE_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal,
BflPromptTemplate.RESPONSE_LANGUAGE,
BflPromptTemplate.ADDRESS_FORM,
BflPromptTemplate.KNOWLEDGE,
BflPromptTemplate.STEP,
BflPromptTemplate.MATERIALS,
BflPromptTemplate.DETAILS,
BflPromptTemplate.MISSING_DETAILS);
/// <summary>
/// The placeholders BflPromptComposer fills in the prompt of the onboarding.
/// </summary>
private static readonly FrozenSet<string> ONBOARDING_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal,
BflPromptTemplate.RESPONSE_LANGUAGE,
BflPromptTemplate.ADDRESS_FORM);
/// <summary>
/// The placeholders of every other file: none, because BflPromptComposer inserts these files as values.
/// </summary>
private static readonly FrozenSet<string> NO_PLACEHOLDERS = FrozenSet<string>.Empty;
/// <summary> /// <summary>
/// Gets the prompt of a step, named like the folder of the step. /// Gets the prompt of a step, named like the folder of the step.
/// </summary> /// </summary>
@@ -66,39 +93,83 @@ public static class BflPromptLibrary
..BflStep.All.Select(StepPath) ..BflStep.All.Select(StepPath)
]; ];
/// <summary>
/// Gets a file of the lab's prompts relative to the folder of the lab, e.g., "Prompts/core.md".
/// </summary>
/// <param name="path">The file, one of the paths of this class.</param>
/// <exception cref="ArgumentException">The file lies outside the folder of the lab.</exception>
public static string RelativePath(string path) => path.StartsWith(FOLDER + "/", StringComparison.Ordinal) ? path[(FOLDER.Length + 1)..] : throw new ArgumentException("The file lies outside the folder of the BeyondFacts Lab.", nameof(path));
/// <summary>
/// Gets the placeholders a file of the lab's prompts may contain.
/// </summary>
/// <remarks>
/// BflPromptComposer fills placeholders in the core and in the prompt of the onboarding only. It inserts every
/// other file as a value, whose placeholders it leaves as they are, so that they would reach the model as text.
/// </remarks>
/// <param name="path">The file, one of the paths of this class.</param>
public static IReadOnlySet<string> PlaceholdersOf(string path) => path switch
{
CORE_PATH => CORE_PLACEHOLDERS,
ONBOARDING_PATH => ONBOARDING_PLACEHOLDERS,
_ => NO_PLACEHOLDERS
};
/// <summary> /// <summary>
/// Reads the texts of the system prompt of a step. /// Reads the texts of the system prompt of a step.
/// </summary> /// </summary>
/// <param name="step">The step.</param> /// <param name="step">The step.</param>
/// <param name="addressForm">How the model addresses the person.</param> /// <param name="addressForm">How the model addresses the person.</param>
/// <param name="overrideDirectory">The folder whose files replace the prompts, or null while the person does not allow it.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The texts, which BflPromptComposer puts together.</returns> /// <returns>The texts, which BflPromptComposer puts together.</returns>
/// <exception cref="FileNotFoundException">The app comes without one of the files.</exception> /// <exception cref="FileNotFoundException">The app comes without one of the files.</exception>
public static async Task<BflPromptParts> LoadPartsAsync(BflStep step, AddressForm addressForm, CancellationToken token = default) => new( /// <exception cref="BflPromptOverrideException">A file which replaces a prompt contains a placeholder which the lab does not fill in it.</exception>
await ReadAsync(CORE_PATH, token), public static async Task<BflPromptParts> LoadPartsAsync(BflStep step, AddressForm addressForm, string? overrideDirectory = null, CancellationToken token = default) => new(
await ReadAsync(KNOWLEDGE_PATH, token), await ReadAsync(CORE_PATH, overrideDirectory, token),
await ReadAsync(StepPath(step), token), await ReadAsync(KNOWLEDGE_PATH, overrideDirectory, token),
await ReadAsync(AddressPath(addressForm), token)); await ReadAsync(StepPath(step), overrideDirectory, token),
await ReadAsync(AddressPath(addressForm), overrideDirectory, token));
/// <summary> /// <summary>
/// Reads the texts of the system prompt of the conversation about whether the lab fits a task. /// Reads the texts of the system prompt of the conversation about whether the lab fits a task.
/// </summary> /// </summary>
/// <param name="addressForm">How the model addresses the person.</param> /// <param name="addressForm">How the model addresses the person.</param>
/// <param name="overrideDirectory">The folder whose files replace the prompts, or null while the person does not allow it.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The texts, which BflPromptComposer puts together.</returns> /// <returns>The texts, which BflPromptComposer puts together.</returns>
/// <exception cref="FileNotFoundException">The app comes without one of the files.</exception> /// <exception cref="FileNotFoundException">The app comes without one of the files.</exception>
public static async Task<BflOnboardingPromptParts> LoadOnboardingPartsAsync(AddressForm addressForm, CancellationToken token = default) => new( /// <exception cref="BflPromptOverrideException">A file which replaces a prompt contains a placeholder which the lab does not fill in it.</exception>
await ReadAsync(ONBOARDING_PATH, token), public static async Task<BflOnboardingPromptParts> LoadOnboardingPartsAsync(AddressForm addressForm, string? overrideDirectory = null, CancellationToken token = default) => new(
await ReadAsync(AddressPath(addressForm), token)); await ReadAsync(ONBOARDING_PATH, overrideDirectory, token),
await ReadAsync(AddressPath(addressForm), overrideDirectory, token));
/// <summary> /// <summary>
/// Reads a file of the lab's prompts. /// Reads a file of the lab's prompts: the file which replaces it, if there is one, or else the file which comes
/// with the app.
/// </summary>
/// <param name="path">The file, one of the paths of this class.</param>
/// <param name="overrideDirectory">The folder whose files replace the prompts, or null while the person does not allow it.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The text of the file.</returns>
/// <exception cref="FileNotFoundException">The app comes without the file, or the file is empty.</exception>
/// <exception cref="BflPromptOverrideException">The file which replaces the prompt contains a placeholder which the lab does not fill in it.</exception>
public static async Task<string> ReadAsync(string path, string? overrideDirectory = null, CancellationToken token = default)
{
if (overrideDirectory is not null && await BflPromptOverrides.TryReadAsync(overrideDirectory, path, token) is { } replacement)
return replacement;
return await ReadBuiltInAsync(path, token);
}
/// <summary>
/// Reads a file of the lab's prompts as it comes with the app.
/// </summary> /// </summary>
/// <param name="path">The file, one of the paths of this class.</param> /// <param name="path">The file, one of the paths of this class.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The text of the file.</returns> /// <returns>The text of the file.</returns>
/// <exception cref="FileNotFoundException">The app comes without the file, or the file is empty.</exception> /// <exception cref="FileNotFoundException">The app comes without the file, or the file is empty.</exception>
public static async Task<string> ReadAsync(string path, CancellationToken token = default) public static async Task<string> ReadBuiltInAsync(string path, CancellationToken token = default)
{ {
var text = await AppResources.ReadTextAsync(path, token); var text = await AppResources.ReadTextAsync(path, token);
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
@@ -0,0 +1,24 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Tells that a file in the folder for prompt overrides contains a placeholder which the lab does not fill in it.
/// </summary>
/// <remarks>
/// The lab fills placeholders in the core and in the prompt of the onboarding only, and only those it knows; in any
/// other file, a placeholder would reach the model as text. Whoever tests a changed prompt needs to learn which file
/// and which placeholder it is, so the message names both.
/// </remarks>
/// <param name="fileName">The file, relative to the folder for prompt overrides, with forward slashes.</param>
/// <param name="placeholder">The name of the placeholder, without braces.</param>
public sealed class BflPromptOverrideException(string fileName, string placeholder) : FormatException($"The file '{fileName}' in the folder for prompt overrides contains the placeholder '{placeholder}', which the lab does not fill in this file.")
{
/// <summary>
/// Gets the file, relative to the folder for prompt overrides, with forward slashes.
/// </summary>
public string FileName { get; } = fileName;
/// <summary>
/// Gets the name of the placeholder, without braces.
/// </summary>
public string Placeholder { get; } = placeholder;
}
@@ -0,0 +1,8 @@
@inherits MSGComponentBase
@if (this.usesPromptOverrides)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true" Class="@this.Class">
@TA("The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.")
</MudAlert>
}
@@ -0,0 +1,72 @@
using AIStudio.Components;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Tells the person that files in the test folder replace prompts of the lab, so that nobody mistakes the answers of
/// experimental prompts for those of the lab.
/// </summary>
/// <remarks>
/// It checks the folder when it appears, e.g., for every step the person opens, and whenever a setting changes. It
/// shows nothing while the setting does not allow prompt overrides, or while every file in the folder matches the
/// prompt it replaces.
/// </remarks>
public partial class BflPromptOverrideNotice : MSGComponentBase
{
[Inject]
private BflConversationService ConversationService { get; init; } = null!;
[Inject]
private ILogger<BflPromptOverrideNotice> Logger { get; init; } = null!;
/// <summary>
/// Gets or sets the CSS classes of the notice, e.g., for its margins.
/// </summary>
[Parameter]
public string Class { get; set; } = string.Empty;
/// <summary>
/// Whether files in the test folder replace prompts of the lab.
/// </summary>
private bool usesPromptOverrides;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]);
await base.OnInitializedAsync();
await this.CheckAsync();
}
/// <inheritdoc />
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED)
{
// The person may switch the prompt overrides on or off in the options of the lab while the page is open:
await this.CheckAsync();
this.StateHasChanged();
}
await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
}
/// <summary>
/// Checks whether files in the test folder replace prompts of the lab.
/// </summary>
private async Task CheckAsync()
{
try
{
this.usesPromptOverrides = await this.ConversationService.UsesPromptOverridesAsync();
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
// Sending reports a problem with the prompts; the notice only stays hidden:
this.Logger.LogDebug(exception, "Could not check the test folder of the BeyondFacts Lab.");
this.usesPromptOverrides = false;
}
}
}
@@ -0,0 +1,157 @@
using System.Text;
using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Reads the files which replace the prompts of the BeyondFacts Lab, so that changed prompts can be tested without a
/// new version of AI Studio.
/// </summary>
/// <remarks>
/// The folder mirrors the files of the lab below Assistants/BeyondFactsLab, e.g., Prompts/steps/02-knowledge.md, so
/// that every name stays unique. Only the files of BflPromptLibrary.AllPaths count; the lab leaves any other file in
/// the folder alone, and an empty file replaces nothing. The files are instructions of the person, who chose to use
/// them, so they bypass the prompt injection filter like the prompts which come with the app.
/// </remarks>
public static class BflPromptOverrides
{
/// <summary>
/// The name of the folder below the folder of the lab.
/// </summary>
public const string DIRECTORY_NAME = "promptOverrides";
/// <summary>
/// Gets the folder whose files replace the prompts of the lab, or null while the setting does not allow them.
/// </summary>
/// <param name="settings">The settings of the lab, which an organization may lock.</param>
/// <param name="directory">The folder for prompt overrides.</param>
public static string? ActiveDirectory(DataBeyondFactsLab settings, string directory) => settings.AllowPromptOverrides ? directory : null;
/// <summary>
/// Reads the file which replaces a prompt of the lab, and checks its placeholders.
/// </summary>
/// <param name="directory">The folder for prompt overrides.</param>
/// <param name="path">The prompt, one of BflPromptLibrary.AllPaths.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The text of the file, or null when no file replaces the prompt.</returns>
/// <exception cref="BflPromptOverrideException">The file contains a placeholder which the lab does not fill in it.</exception>
public static async Task<string?> TryReadAsync(string directory, string path, CancellationToken token = default)
{
var text = await TryReadFileAsync(directory, path, token);
if (text is null)
return null;
var allowed = BflPromptLibrary.PlaceholdersOf(path);
foreach (var placeholder in BflPromptTemplate.NamesIn(text))
{
if (!allowed.Contains(placeholder))
throw new BflPromptOverrideException(BflPromptLibrary.RelativePath(path), placeholder);
}
return text;
}
/// <summary>
/// Finds the prompts of the lab which a file in the folder replaces with another text.
/// </summary>
/// <remarks>
/// A copy of a prompt which matches the prompt of this version, apart from line breaks and the space around the
/// text, changes nothing and does not count. A copy of an older version does.
/// </remarks>
/// <param name="directory">The folder for prompt overrides.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The prompts, as paths of BflPromptLibrary.AllPaths.</returns>
public static async Task<IReadOnlyList<string>> FindAsync(string directory, CancellationToken token = default)
{
var replaced = new List<string>();
foreach (var path in BflPromptLibrary.AllPaths)
{
string? text;
try
{
text = await TryReadFileAsync(directory, path, token);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
// The lab will try to read the file, too, so it replaces the prompt as far as the person can tell:
replaced.Add(path);
continue;
}
if (text is not null && Normalize(text) != Normalize(await BflPromptLibrary.ReadBuiltInAsync(path, token)))
replaced.Add(path);
}
return replaced;
}
/// <summary>
/// Copies the prompts of this version into the folder, so that changes start from them. A file which exists stays
/// as it is.
/// </summary>
/// <param name="directory">The folder for prompt overrides; it is created when it is missing.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>How many files the folder got.</returns>
public static async Task<int> CopyBuiltInAsync(string directory, CancellationToken token = default)
{
var copied = 0;
foreach (var path in BflPromptLibrary.AllPaths)
{
var target = FilePath(directory, path);
if (File.Exists(target))
continue;
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
var temporary = $"{target}.{Guid.NewGuid():N}.tmp";
await File.WriteAllTextAsync(temporary, await BflPromptLibrary.ReadBuiltInAsync(path, token), new UTF8Encoding(false), token);
try
{
// Moving fails when the file appeared meanwhile, so the person's file is never overwritten:
File.Move(temporary, target, false);
copied++;
}
catch (IOException) when (File.Exists(target))
{
File.Delete(temporary);
}
}
return copied;
}
/// <summary>
/// Gets the file which replaces a prompt of the lab.
/// </summary>
/// <param name="directory">The folder for prompt overrides.</param>
/// <param name="path">The prompt, one of BflPromptLibrary.AllPaths.</param>
public static string FilePath(string directory, string path) => Path.Join([directory, ..BflPromptLibrary.RelativePath(path).Split('/')]);
/// <summary>
/// Reads the file which replaces a prompt of the lab, without checking it.
/// </summary>
/// <param name="directory">The folder for prompt overrides.</param>
/// <param name="path">The prompt, one of BflPromptLibrary.AllPaths.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The text of the file, or null when there is no such file or it is empty.</returns>
private static async Task<string?> TryReadFileAsync(string directory, string path, CancellationToken token)
{
string text;
try
{
text = await File.ReadAllTextAsync(FilePath(directory, path), Encoding.UTF8, token);
}
catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException)
{
return null;
}
return string.IsNullOrWhiteSpace(text) ? null : text;
}
/// <summary>
/// Brings a prompt into the form in which two copies compare, whatever system wrote them.
/// </summary>
/// <param name="text">The prompt.</param>
private static string Normalize(string text) => text.ReplaceLineEndings("\n").Trim();
}
@@ -60,6 +60,13 @@ public static partial class BflPromptTemplate
return values.TryGetValue(name, out var value) ? value : throw new FormatException($"The prompt has the placeholder '{name}', which has no value."); return values.TryGetValue(name, out var value) ? value : throw new FormatException($"The prompt has the placeholder '{name}', which has no value.");
}); });
/// <summary>
/// Finds the placeholders of a template, as Fill reads them.
/// </summary>
/// <param name="template">The template.</param>
/// <returns>The name of each placeholder, once each, in the order they first appear.</returns>
public static IReadOnlyList<string> NamesIn(string template) => [.. PlaceholderPattern().Matches(template).Select(match => match.Groups["name"].Value).Distinct(StringComparer.Ordinal)];
[GeneratedRegex(@"\{\{\s*(?<name>[^{}]*?)\s*\}\}")] [GeneratedRegex(@"\{\{\s*(?<name>[^{}]*?)\s*\}\}")]
private static partial Regex PlaceholderPattern(); private static partial Regex PlaceholderPattern();
} }
@@ -970,6 +970,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295"] = "
-- This conversation fills most of the context window of the model. Soon, you will have to start it over. -- This conversation fills most of the context window of the model. Soon, you will have to start it over.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295::INFORMAL"] = "This conversation fills most of the context window of the model. Soon, you will have to start it over." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295::INFORMAL"] = "This conversation fills most of the context window of the model. Soon, you will have to start it over."
-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1150375027"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again."
-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1150375027::INFORMAL"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again."
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1809312323"] = "Please select a provider." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1809312323"] = "Please select a provider."
@@ -1459,6 +1465,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T180931232
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T1809312323::INFORMAL"] = "Please select a provider." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T1809312323::INFORMAL"] = "Please select a provider."
-- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521"] = "The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab."
-- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521::INFORMAL"] = "The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab."
-- Test situation -- Test situation
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Test situation" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Test situation"
@@ -10405,9 +10417,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T82
-- Restore default -- Restore default
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T913168756"] = "Restore default" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T913168756"] = "Restore default"
-- Hide Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1108876344"] = "Hide Expert Settings"
-- Could not open the test folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1196482976"] = "Could not open the test folder."
-- The test folder holds every prompt already. AI Studio changed no file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1367912036"] = "The test folder holds every prompt already. AI Studio changed no file."
-- Could not open the test folder: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1478589470"] = "Could not open the test folder: {0}"
-- These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T15706444"] = "These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance."
-- These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T15706444::INFORMAL"] = "These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance."
-- The lab uses its own prompts
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1931375136"] = "The lab uses its own prompts"
-- Assistant: BeyondFacts Lab Options -- Assistant: BeyondFacts Lab Options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T19824556"] = "Assistant: BeyondFacts Lab Options" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T19824556"] = "Assistant: BeyondFacts Lab Options"
-- Files copied into the test folder: {0}. Files which were there already stay as they are.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2064303845"] = "Files copied into the test folder: {0}. Files which were there already stay as they are."
-- A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2174086011"] = "A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step."
-- A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2174086011::INFORMAL"] = "A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step."
-- Form of address -- Form of address
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2183481736"] = "Form of address" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2183481736"] = "Form of address"
@@ -10417,9 +10459,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T257
-- Select a minimum confidence level -- Select a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2579793544::INFORMAL"] = "Select a minimum confidence level" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2579793544::INFORMAL"] = "Select a minimum confidence level"
-- Use prompts from the test folder?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2718589875"] = "Use prompts from the test folder?"
-- Copy the built-in prompts into the test folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2787777151"] = "Copy the built-in prompts into the test folder"
-- Files in the test folder replace the prompts of the lab
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T320994561"] = "Files in the test folder replace the prompts of the lab"
-- Show Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3361153305"] = "Show Expert Settings"
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3448155331"] = "Close" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3448155331"] = "Close"
-- Unknown error
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3461425987"] = "Unknown error"
-- Could not copy the prompts into the test folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3478020269"] = "Could not copy the prompts into the test folder."
-- Whether the lab addresses you formally or informally. -- Whether the lab addresses you formally or informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3883838251"] = "Whether the lab addresses you formally or informally." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3883838251"] = "Whether the lab addresses you formally or informally."
@@ -10432,6 +10492,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T400
-- Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models. -- Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T4002746484::INFORMAL"] = "Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T4002746484::INFORMAL"] = "Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models."
-- Open test folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T567935877"] = "Open test folder"
-- Files are sent as they were first read -- Files are sent as they were first read
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1320692831"] = "Files are sent as they were first read" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1320692831"] = "Files are sent as they were first read"
@@ -17,6 +17,27 @@
<ConfigurationSelect OptionDescription="@TA("Select a minimum confidence level")" SelectedValue="@this.SelectedMinimumConfidence" Data="@ConfigurationSelectDataFactory.GetBeyondFactsLabConfidenceLevelsData(this.SettingsManager)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BeyondFactsLab.MinimumProviderConfidence = value)" OptionHelp="@TA("Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BeyondFactsLab, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/> <ConfigurationSelect OptionDescription="@TA("Select a minimum confidence level")" SelectedValue="@this.SelectedMinimumConfidence" Data="@ConfigurationSelectDataFactory.GetBeyondFactsLabConfidenceLevelsData(this.SettingsManager)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BeyondFactsLab.MinimumProviderConfidence = value)" OptionHelp="@TA("Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BeyondFactsLab, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
<ConfigurationProviderSelection Component="Components.BEYOND_FACTS_LAB_ASSISTANT" Data="@this.AvailableLLMProviders" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BeyondFactsLab.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BeyondFactsLab.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BeyondFactsLab, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/> <ConfigurationProviderSelection Component="Components.BEYOND_FACTS_LAB_ASSISTANT" Data="@this.AvailableLLMProviders" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BeyondFactsLab.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BeyondFactsLab.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BeyondFactsLab, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
</MudPaper> </MudPaper>
@* Only whoever develops the prompts further needs these settings, so they stay folded away. *@
<MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune">
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
</MudButton>
<MudCollapse Expanded="@this.showExpertSettings">
<MudPaper Class="pa-3 mt-3 mb-8 border-dashed border rounded-lg">
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@TA("These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.")
</MudJustifiedText>
<ConfigurationOption OptionDescription="@T("Use prompts from the test folder?")" LabelOn="@T("Files in the test folder replace the prompts of the lab")" LabelOff="@T("The lab uses its own prompts")" State="@(() => this.SettingsManager.ConfigurationData.BeyondFactsLab.AllowPromptOverrides)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BeyondFactsLab.AllowPromptOverrides = value)" OptionHelp="@TA("A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.")" IsLocked="() => IsPromptOverridesLocked"/>
<MudStack Row="true" Wrap="Wrap.Wrap" Spacing="2" Class="mt-3">
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" OnClick="@this.OpenTestFolderAsync" Disabled="@this.ArePromptOverridesLockedOff">
@T("Open test folder")
</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.ContentCopy" Variant="Variant.Outlined" OnClick="@this.CopyPromptsAsync" Disabled="@(this.ArePromptOverridesLockedOff || this.isCopyingPrompts)">
@T("Copy the built-in prompts into the test folder")
</MudButton>
</MudStack>
</MudPaper>
</MudCollapse>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton> <MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
@@ -1,5 +1,10 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs.Settings; namespace AIStudio.Dialogs.Settings;
@@ -8,12 +13,39 @@ namespace AIStudio.Dialogs.Settings;
/// </summary> /// </summary>
public partial class SettingsDialogBeyondFactsLab : SettingsDialogBase public partial class SettingsDialogBeyondFactsLab : SettingsDialogBase
{ {
[Inject]
private BeyondFactsLabStore Store { get; init; } = null!;
[Inject]
private ILogger<SettingsDialogBeyondFactsLab> Logger { get; init; } = null!;
/// <summary> /// <summary>
/// Whether the language of the app tells a formal address from an informal one, so that the form of address /// Whether the language of the app tells a formal address from an informal one, so that the form of address
/// is a choice at all. /// is a choice at all.
/// </summary> /// </summary>
private bool offersFormOfAddress; private bool offersFormOfAddress;
/// <summary>
/// Whether the settings for the further development of the lab are unfolded.
/// </summary>
private bool showExpertSettings;
/// <summary>
/// Whether the prompts are being copied into the test folder right now.
/// </summary>
private bool isCopyingPrompts;
/// <summary>
/// Gets whether an organization manages the prompt overrides.
/// </summary>
private static bool IsPromptOverridesLocked => ManagedConfiguration.TryGet(x => x.BeyondFactsLab, x => x.AllowPromptOverrides, out var meta) && meta.IsLocked;
/// <summary>
/// Gets whether an organization switched the prompt overrides off for good, so that the test folder serves no
/// purpose.
/// </summary>
private bool ArePromptOverridesLockedOff => IsPromptOverridesLocked && !this.SettingsManager.ConfigurationData.BeyondFactsLab.AllowPromptOverrides;
/// <inheritdoc /> /// <inheritdoc />
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -29,4 +61,57 @@ public partial class SettingsDialogBeyondFactsLab : SettingsDialogBase
/// offer it either. Showing the floor instead tells what really counts. /// offer it either. Showing the floor instead tells what really counts.
/// </remarks> /// </remarks>
private ConfidenceLevel SelectedMinimumConfidence() => Tools.Components.BEYOND_FACTS_LAB_ASSISTANT.MinimumConfidence(this.SettingsManager); private ConfidenceLevel SelectedMinimumConfidence() => Tools.Components.BEYOND_FACTS_LAB_ASSISTANT.MinimumConfidence(this.SettingsManager);
private void ToggleExpertSettings() => this.showExpertSettings = !this.showExpertSettings;
/// <summary>
/// Opens the test folder in the file manager, and creates it first when it is missing.
/// </summary>
private async Task OpenTestFolderAsync()
{
OpenPathResponse response;
try
{
var directory = this.Store.PromptOverridesDirectory;
Directory.CreateDirectory(directory);
response = await this.RustService.TryOpenPathInRuntimeFileManager(directory);
}
catch (Exception exception)
{
this.Logger.LogWarning(exception, "Could not open the test folder of the BeyondFacts Lab.");
await this.MessageBus.SendError(new(Icons.Material.Filled.FolderOpen, T("Could not open the test folder.")));
return;
}
if (response.Success)
return;
var issue = string.IsNullOrWhiteSpace(response.Issue) ? T("Unknown error") : response.Issue;
await this.MessageBus.SendError(new(Icons.Material.Filled.FolderOpen, string.Format(T("Could not open the test folder: {0}"), issue)));
}
/// <summary>
/// Copies the prompts of this version into the test folder, so that changes start from them. A file which exists
/// stays as it is.
/// </summary>
private async Task CopyPromptsAsync()
{
this.isCopyingPrompts = true;
try
{
var copied = await BflPromptOverrides.CopyBuiltInAsync(this.Store.PromptOverridesDirectory);
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.ContentCopy, copied is 0
? T("The test folder holds every prompt already. AI Studio changed no file.")
: string.Format(T("Files copied into the test folder: {0}. Files which were there already stay as they are."), copied)));
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
this.Logger.LogWarning(exception, "Could not copy the prompts of the BeyondFacts Lab into the test folder.");
await this.MessageBus.SendError(new(Icons.Material.Filled.ContentCopy, T("Could not copy the prompts into the test folder.")));
}
finally
{
this.isCopyingPrompts = false;
}
}
} }
@@ -1002,6 +1002,13 @@ CONFIG["SETTINGS"] = {}
-- of the lab do not show it. -- of the lab do not show it.
-- Allowed values are: FORMAL, INFORMAL -- Allowed values are: FORMAL, INFORMAL
-- CONFIG["SETTINGS"]["DataBeyondFactsLab.AddressForm"] = "FORMAL" -- CONFIG["SETTINGS"]["DataBeyondFactsLab.AddressForm"] = "FORMAL"
--
-- Configure whether files in the folder for prompt overrides replace the prompts which come with the lab.
-- The option serves the further development of the prompts, e.g., by the research team behind the lab.
-- Preparing an appearance does not need it. Such files count as instructions of the users themselves,
-- and the lab shows a banner while one of them replaces a prompt. Set it to false and lock it to keep
-- users with the prompts which come with AI Studio.
-- CONFIG["SETTINGS"]["DataBeyondFactsLab.AllowPromptOverrides"] = false
-- Configure defaults for the Assistant Builder. -- Configure defaults for the Assistant Builder.
-- Preselection must be enabled for the remaining Assistant Builder settings to take effect. -- Preselection must be enabled for the remaining Assistant Builder settings to take effect.
@@ -972,6 +972,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295"] = "
-- This conversation fills most of the context window of the model. Soon, you will have to start it over. -- This conversation fills most of the context window of the model. Soon, you will have to start it over.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295::INFORMAL"] = "Dieses Gespräch füllt den größten Teil des Kontextfensters des Modells. Bald musst du es neu beginnen." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295::INFORMAL"] = "Dieses Gespräch füllt den größten Teil des Kontextfensters des Modells. Bald musst du es neu beginnen."
-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1150375027"] = "Die Datei „{0}“ im Testordner des BeyondFacts Labs enthält den Platzhalter „{1}“, den das Lab in dieser Datei nicht ausfüllt. Bitte korrigieren Sie die Datei und versuchen Sie es erneut."
-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1150375027::INFORMAL"] = "Die Datei „{0}“ im Testordner des BeyondFacts Labs enthält den Platzhalter „{1}“, den das Lab in dieser Datei nicht ausfüllt. Bitte korrigiere die Datei und versuche es erneut."
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1809312323"] = "Bitte wählen Sie einen Anbieter aus." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1809312323"] = "Bitte wählen Sie einen Anbieter aus."
@@ -1461,6 +1467,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T180931232
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T1809312323::INFORMAL"] = "Bitte wähle einen Anbieter aus." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T1809312323::INFORMAL"] = "Bitte wähle einen Anbieter aus."
-- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521"] = "Das Lab verwendet derzeit experimentelle Anweisungen aus seinem Testordner. Sie können sie in den Optionen des Labs deaktivieren."
-- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521::INFORMAL"] = "Das Lab verwendet derzeit experimentelle Anweisungen aus seinem Testordner. Du kannst sie in den Optionen des Labs deaktivieren."
-- Test situation -- Test situation
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Testsituation" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Testsituation"
@@ -10407,9 +10419,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T82
-- Restore default -- Restore default
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T913168756"] = "Standard wiederherstellen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T913168756"] = "Standard wiederherstellen"
-- Hide Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1108876344"] = "Experten-Einstellungen ausblenden"
-- Could not open the test folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1196482976"] = "Der Testordner konnte nicht geöffnet werden."
-- The test folder holds every prompt already. AI Studio changed no file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1367912036"] = "Der Testordner enthält bereits alle Prompts. AI Studio hat keine Datei geändert."
-- Could not open the test folder: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1478589470"] = "Der Testordner konnte nicht geöffnet werden: {0}"
-- These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T15706444"] = "Diese Einstellungen dienen der Weiterentwicklung des Labs, z. B. durch das Forschungsteam hinter dem Lab. Zur Vorbereitung eines Auftritts brauchen Sie sie nicht."
-- These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T15706444::INFORMAL"] = "Diese Einstellungen dienen der Weiterentwicklung des Labs, z. B. durch das Forschungsteam hinter dem Lab. Zur Vorbereitung eines Auftritts brauchst du sie nicht."
-- The lab uses its own prompts
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1931375136"] = "Das Lab verwendet seine eigenen Prompts"
-- Assistant: BeyondFacts Lab Options -- Assistant: BeyondFacts Lab Options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T19824556"] = "Assistent: Optionen für das BeyondFacts Lab" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T19824556"] = "Assistent: Optionen für das BeyondFacts Lab"
-- Files copied into the test folder: {0}. Files which were there already stay as they are.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2064303845"] = "In den Testordner kopierte Dateien: {0}. Dateien, die bereits dort vorhanden waren, bleiben unverändert."
-- A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2174086011"] = "Eine Datei im Testordner ersetzt den gleichnamigen Prompt, z. B. „Prompts/core.md“. Das Lab verwendet alle anderen Prompts aus AI Studio. Solange eine Datei einen Prompt ersetzt, zeigt das Lab über jedem Schritt einen Hinweis an."
-- A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2174086011::INFORMAL"] = "Eine Datei im Testordner ersetzt den gleichnamigen Prompt, z. B. „Prompts/core.md“. Das Lab verwendet alle anderen Prompts aus AI Studio. Solange eine Datei einen Prompt ersetzt, zeigt das Lab über jedem Schritt einen Hinweis an."
-- Form of address -- Form of address
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2183481736"] = "Anrede" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2183481736"] = "Anrede"
@@ -10419,9 +10461,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T257
-- Select a minimum confidence level -- Select a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2579793544::INFORMAL"] = "Wähle ein minimales Vertrauensniveau aus" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2579793544::INFORMAL"] = "Wähle ein minimales Vertrauensniveau aus"
-- Use prompts from the test folder?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2718589875"] = "Prompts aus dem Testordner verwenden?"
-- Copy the built-in prompts into the test folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2787777151"] = "Mitgelieferte Prompts in den Testordner kopieren"
-- Files in the test folder replace the prompts of the lab
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T320994561"] = "Dateien im Testordner ersetzen die Prompts des Labs"
-- Show Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3361153305"] = "Experten-Einstellungen anzeigen"
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3448155331"] = "Schließen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3448155331"] = "Schließen"
-- Unknown error
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3461425987"] = "Unbekannter Fehler"
-- Could not copy the prompts into the test folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3478020269"] = "Die Prompts konnten nicht in den Testordner kopiert werden."
-- Whether the lab addresses you formally or informally. -- Whether the lab addresses you formally or informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3883838251"] = "Ob das Lab Sie siezt oder duzt." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3883838251"] = "Ob das Lab Sie siezt oder duzt."
@@ -10434,6 +10494,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T400
-- Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models. -- Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T4002746484::INFORMAL"] = "Deine Vorbereitungen enthalten meist unveröffentlichte Forschung. Daher akzeptiert das Lab nur Anbieter mit mindestens dem Vertrauensniveau „Mittel“. Du kannst diese Untergrenze anheben, etwa um nur selbst gehostete Modelle zu verwenden." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T4002746484::INFORMAL"] = "Deine Vorbereitungen enthalten meist unveröffentlichte Forschung. Daher akzeptiert das Lab nur Anbieter mit mindestens dem Vertrauensniveau „Mittel“. Du kannst diese Untergrenze anheben, etwa um nur selbst gehostete Modelle zu verwenden."
-- Open test folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T567935877"] = "Testordner öffnen"
-- Files are sent as they were first read -- Files are sent as they were first read
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1320692831"] = "Dateien werden so gesendet, wie sie zuerst eingelesen wurden" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1320692831"] = "Dateien werden so gesendet, wie sie zuerst eingelesen wurden"
@@ -972,6 +972,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295"] = "
-- This conversation fills most of the context window of the model. Soon, you will have to start it over. -- This conversation fills most of the context window of the model. Soon, you will have to start it over.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295::INFORMAL"] = "This conversation fills most of the context window of the model. Soon, you will have to start it over." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCHAT::T991103295::INFORMAL"] = "This conversation fills most of the context window of the model. Soon, you will have to start it over."
-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1150375027"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again."
-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1150375027::INFORMAL"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again."
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1809312323"] = "Please select a provider." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLCONVERSATIONSERVICE::T1809312323"] = "Please select a provider."
@@ -1461,6 +1467,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T180931232
-- Please select a provider. -- Please select a provider.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T1809312323::INFORMAL"] = "Please select a provider." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLONBOARDING::T1809312323::INFORMAL"] = "Please select a provider."
-- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521"] = "The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab."
-- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521::INFORMAL"] = "The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab."
-- Test situation -- Test situation
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Test situation" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Test situation"
@@ -10407,9 +10419,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T82
-- Restore default -- Restore default
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T913168756"] = "Restore default" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T913168756"] = "Restore default"
-- Hide Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1108876344"] = "Hide Expert Settings"
-- Could not open the test folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1196482976"] = "Could not open the test folder."
-- The test folder holds every prompt already. AI Studio changed no file.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1367912036"] = "The test folder holds every prompt already. AI Studio changed no file."
-- Could not open the test folder: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1478589470"] = "Could not open the test folder: {0}"
-- These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T15706444"] = "These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance."
-- These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T15706444::INFORMAL"] = "These settings serve the further development of the lab, e.g., by the research team behind it. You do not need them to prepare an appearance."
-- The lab uses its own prompts
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T1931375136"] = "The lab uses its own prompts"
-- Assistant: BeyondFacts Lab Options -- Assistant: BeyondFacts Lab Options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T19824556"] = "Assistant: BeyondFacts Lab Options" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T19824556"] = "Assistant: BeyondFacts Lab Options"
-- Files copied into the test folder: {0}. Files which were there already stay as they are.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2064303845"] = "Files copied into the test folder: {0}. Files which were there already stay as they are."
-- A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2174086011"] = "A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step."
-- A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2174086011::INFORMAL"] = "A file in the test folder replaces the prompt of the same name, e.g., 'Prompts/core.md'. The lab takes every other prompt from AI Studio. While a file replaces a prompt, the lab shows a notice above every step."
-- Form of address -- Form of address
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2183481736"] = "Form of address" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2183481736"] = "Form of address"
@@ -10419,9 +10461,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T257
-- Select a minimum confidence level -- Select a minimum confidence level
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2579793544::INFORMAL"] = "Select a minimum confidence level" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2579793544::INFORMAL"] = "Select a minimum confidence level"
-- Use prompts from the test folder?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2718589875"] = "Use prompts from the test folder?"
-- Copy the built-in prompts into the test folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T2787777151"] = "Copy the built-in prompts into the test folder"
-- Files in the test folder replace the prompts of the lab
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T320994561"] = "Files in the test folder replace the prompts of the lab"
-- Show Expert Settings
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3361153305"] = "Show Expert Settings"
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3448155331"] = "Close" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3448155331"] = "Close"
-- Unknown error
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3461425987"] = "Unknown error"
-- Could not copy the prompts into the test folder.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3478020269"] = "Could not copy the prompts into the test folder."
-- Whether the lab addresses you formally or informally. -- Whether the lab addresses you formally or informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3883838251"] = "Whether the lab addresses you formally or informally." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T3883838251"] = "Whether the lab addresses you formally or informally."
@@ -10434,6 +10494,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T400
-- Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models. -- Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T4002746484::INFORMAL"] = "Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T4002746484::INFORMAL"] = "Your preparations usually contain unpublished research. Therefore, the lab accepts only providers with at least medium confidence. You can raise this minimum, e.g., to use only self-hosted models."
-- Open test folder
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBEYONDFACTSLAB::T567935877"] = "Open test folder"
-- Files are sent as they were first read -- Files are sent as they were first read
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1320692831"] = "Files are sent as they were first read" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1320692831"] = "Files are sent as they were first read"
@@ -45,6 +45,15 @@ public sealed class DataBeyondFactsLab(Expression<Func<Data, DataBeyondFactsLab>
/// </summary> /// </summary>
public AddressForm AddressForm { get; set; } = ManagedConfiguration.Register(configSelection, value => value.AddressForm, AddressForm.FORMAL); public AddressForm AddressForm { get; set; } = ManagedConfiguration.Register(configSelection, value => value.AddressForm, AddressForm.FORMAL);
/// <summary>
/// Gets or sets whether files in the folder for prompt overrides replace the prompts which come with the lab.
/// </summary>
/// <remarks>
/// The option serves the further development of the prompts, e.g., by the research team behind the lab, not the
/// preparation of an appearance. An organization may switch it off and lock it.
/// </remarks>
public bool AllowPromptOverrides { get; set; } = ManagedConfiguration.Register(configSelection, value => value.AllowPromptOverrides, false);
/// <summary> /// <summary>
/// Gets or sets the hash of the notes the person confirmed, see BflNotice; empty while they confirmed none. /// Gets or sets the hash of the notes the person confirmed, see BflNotice; empty while they confirmed none.
/// </summary> /// </summary>
@@ -520,6 +520,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.AddressForm, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.AddressForm, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.AllowPromptOverrides, this.Id, settingsTable, dryRun);
// Config: Assistant Builder defaults? // Config: Assistant Builder defaults?
ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectOptions, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
@@ -55,6 +55,32 @@ public sealed class BflPromptLibraryTests
Assert.That(BflPromptLibrary.AddressPath(AddressForm.INFORMAL), Is.Not.EqualTo(BflPromptLibrary.AddressPath(AddressForm.FORMAL))); Assert.That(BflPromptLibrary.AddressPath(AddressForm.INFORMAL), Is.Not.EqualTo(BflPromptLibrary.AddressPath(AddressForm.FORMAL)));
} }
[Test]
public async Task EveryPromptUsesOnlyThePlaceholdersTheLabFillsInIt()
{
foreach (var path in BflPromptLibrary.AllPaths)
Assert.That(BflPromptTemplate.NamesIn(await BflPromptLibrary.ReadBuiltInAsync(path)), Is.SubsetOf(BflPromptLibrary.PlaceholdersOf(path)), path);
}
[Test]
public void TheComposerFillsEveryPlaceholderAPromptMayContain()
{
var core = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.CORE_PATH).Select(name => $"{{{{{name}}}}}"));
var onboarding = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.ONBOARDING_PATH).Select(name => $"{{{{{name}}}}}"));
using (Assert.EnterMultipleScope())
{
Assert.That(BflPromptComposer.Compose(new(core, "Leitfaden", "Schritt", "Anrede"), BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch"), Does.Not.Contain("{{"));
Assert.That(BflPromptComposer.ComposeOnboarding(new(onboarding, "Anrede"), "Deutsch"), Does.Not.Contain("{{"));
}
}
[Test]
public void EveryPromptLiesInTheFolderOfTheLab()
{
Assert.That(BflPromptLibrary.AllPaths.Select(BflPromptLibrary.RelativePath), Is.Unique.And.All.Not.StartWith("/"));
}
[Test] [Test]
public async Task AMissingPromptIsAnError() public async Task AMissingPromptIsAnError()
{ {
@@ -0,0 +1,74 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Settings;
using Lua;
using Lua.Standard;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks that an organization can keep the prompts of the BeyondFacts Lab as they come with AI Studio.
/// </summary>
/// <remarks>
/// The settings are reached through Program.SERVICE_PROVIDER, as in the app, because a managed setting writes its
/// value and its lock there.
/// </remarks>
[TestFixture]
public sealed class BflPromptOverrideSettingTests
{
private static readonly Guid PLUGIN_ID = Guid.Parse("4f0c2b7e-8d31-4a96-b5e2-7c1d9a3e6f08");
private SettingsManager settingsManager = null!;
private ServiceProvider serviceProvider = null!;
private IServiceProvider previousServiceProvider = null!;
[SetUp]
public void CreateSettings()
{
// Creating the settings data registers every managed setting:
this.settingsManager = new SettingsManager(NullLogger<SettingsManager>.Instance, null!);
this.previousServiceProvider = Program.SERVICE_PROVIDER;
this.serviceProvider = new ServiceCollection().AddSingleton(this.settingsManager).BuildServiceProvider();
Program.SERVICE_PROVIDER = this.serviceProvider;
}
[TearDown]
public void RestoreApplicationState()
{
// The managed state of a setting outlives this fixture, so we release what the plugin took over:
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
Program.SERVICE_PROVIDER = this.previousServiceProvider;
this.serviceProvider.Dispose();
}
[Test]
public async Task AnOrganizationCanSwitchOffThePromptOverridesForGood()
{
this.settingsManager.ConfigurationData.BeyondFactsLab.AllowPromptOverrides = true;
var state = LuaState.Create();
state.OpenBasicLibrary();
await state.DoStringAsync("""
SETTINGS = {}
SETTINGS["DataBeyondFactsLab.AllowPromptOverrides"] = false
""");
if (!state.Environment["SETTINGS"].TryRead<LuaTable>(out var settingsTable))
throw new InvalidOperationException("The settings of this test are not a Lua table.");
var processed = ManagedConfiguration.TryProcessConfiguration(x => x.BeyondFactsLab, x => x.AllowPromptOverrides, PLUGIN_ID, settingsTable, false);
ManagedConfiguration.TryGet(x => x.BeyondFactsLab, x => x.AllowPromptOverrides, out var configMeta);
using (Assert.EnterMultipleScope())
{
Assert.That(processed, Is.True);
Assert.That(configMeta.IsLocked, Is.True);
Assert.That(BflPromptOverrides.ActiveDirectory(this.settingsManager.ConfigurationData.BeyondFactsLab, "prompts"), Is.Null);
}
}
}
@@ -0,0 +1,243 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks how files in the folder for prompt overrides replace the prompts of the BeyondFacts Lab.
/// </summary>
/// <remarks>
/// The folder serves whoever develops the prompts further. A file there replaces the prompt of the same name only;
/// what the folder lacks, or what it holds besides, changes nothing.
/// </remarks>
[TestFixture]
public sealed class BflPromptOverridesTests
{
private string directory = string.Empty;
[SetUp]
public void CreateDirectory()
{
this.directory = Path.Combine(Path.GetTempPath(), $"ai-studio-beyondfacts-lab-prompts-{Guid.NewGuid():N}");
}
[TearDown]
public void DeleteDirectory()
{
if (Directory.Exists(this.directory))
Directory.Delete(this.directory, true);
}
[Test]
public void TheFolderMirrorsTheFilesOfTheLab()
{
var path = BflPromptOverrides.FilePath(this.directory, BflPromptLibrary.StepPath(BflStep.KNOWLEDGE));
Assert.That(path, Is.EqualTo(Path.Combine(this.directory, "Prompts", "steps", $"{BflStep.KNOWLEDGE.FolderName}.md")));
}
[Test]
public void EveryPromptHasAFileOfItsOwnInTheFolder()
{
var files = BflPromptLibrary.AllPaths.Select(path => BflPromptOverrides.FilePath(this.directory, path)).ToList();
Assert.That(files, Is.Unique);
}
[Test]
public async Task AFileInTheFolderReplacesThePromptOfTheSameNameOnly()
{
var replaced = BflPromptLibrary.StepPath(BflStep.GOALS_AND_ROLE);
await this.WriteAsync(replaced, "Ein neuer Prompt für die Ziele.");
var parts = await BflPromptLibrary.LoadPartsAsync(BflStep.GOALS_AND_ROLE, AddressForm.FORMAL, this.directory);
var otherStep = await BflPromptLibrary.ReadAsync(BflPromptLibrary.StepPath(BflStep.VALUES_AND_PERSPECTIVES), this.directory);
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Step, Is.EqualTo("Ein neuer Prompt für die Ziele."));
Assert.That(parts.Core, Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(BflPromptLibrary.CORE_PATH)));
Assert.That(otherStep, Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(BflPromptLibrary.StepPath(BflStep.VALUES_AND_PERSPECTIVES))));
Assert.That(await BflPromptOverrides.FindAsync(this.directory), Is.EqualTo(new[] { replaced }));
}
}
[Test]
public async Task WithoutTheSettingTheFilesReplaceNothing()
{
await this.WriteAsync(BflPromptLibrary.CORE_PATH, "Ein neuer Kern.");
var settings = new DataBeyondFactsLab { AllowPromptOverrides = false };
var activeDirectory = BflPromptOverrides.ActiveDirectory(settings, this.directory);
var core = await BflPromptLibrary.ReadAsync(BflPromptLibrary.CORE_PATH, activeDirectory);
using (Assert.EnterMultipleScope())
{
Assert.That(activeDirectory, Is.Null);
Assert.That(core, Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(BflPromptLibrary.CORE_PATH)));
Assert.That(BflPromptOverrides.ActiveDirectory(new DataBeyondFactsLab { AllowPromptOverrides = true }, this.directory), Is.EqualTo(this.directory));
}
}
[Test]
public async Task AMissingFolderReplacesNothing()
{
using (Assert.EnterMultipleScope())
{
Assert.That(await BflPromptLibrary.ReadAsync(BflPromptLibrary.ONBOARDING_PATH, this.directory), Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(BflPromptLibrary.ONBOARDING_PATH)));
Assert.That(await BflPromptOverrides.FindAsync(this.directory), Is.Empty);
}
}
[Test]
public async Task AnEmptyFileReplacesNothing()
{
await this.WriteAsync(BflPromptLibrary.ONBOARDING_PATH, " \n");
using (Assert.EnterMultipleScope())
{
Assert.That(await BflPromptLibrary.ReadAsync(BflPromptLibrary.ONBOARDING_PATH, this.directory), Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(BflPromptLibrary.ONBOARDING_PATH)));
Assert.That(await BflPromptOverrides.FindAsync(this.directory), Is.Empty);
}
}
[Test]
public async Task AnUnknownFileChangesNothing()
{
Directory.CreateDirectory(Path.Combine(this.directory, "Prompts", "steps"));
await File.WriteAllTextAsync(Path.Combine(this.directory, "Prompts", "steps", "99-unknown.md"), "Ein Schritt, den es nicht gibt.");
await File.WriteAllTextAsync(Path.Combine(this.directory, "core.md"), "Ein Kern am falschen Ort.");
await File.WriteAllTextAsync(Path.Combine(this.directory, "notes.txt"), "Notizen von {{NIEMANDEM}}.");
using (Assert.EnterMultipleScope())
{
Assert.That(await BflPromptLibrary.ReadAsync(BflPromptLibrary.CORE_PATH, this.directory), Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(BflPromptLibrary.CORE_PATH)));
Assert.That(await BflPromptOverrides.FindAsync(this.directory), Is.Empty);
}
}
[Test]
public async Task AFileWithAPlaceholderTheLabDoesNotFillThereIsAnError()
{
await this.WriteAsync(BflPromptLibrary.StepPath(BflStep.STRATEGY), "Beachte {{DETAILS}}.");
await this.WriteAsync(BflPromptLibrary.CORE_PATH, "{{STEP}} {{UNKNOWN}}");
await this.WriteAsync(BflPromptLibrary.ONBOARDING_PATH, "{{RESPONSE_LANGUAGE}} {{KNOWLEDGE}}");
var inStep = await Assert.ThrowsAsync<BflPromptOverrideException>(async () => await BflPromptLibrary.ReadAsync(BflPromptLibrary.StepPath(BflStep.STRATEGY), this.directory));
var inCore = await Assert.ThrowsAsync<BflPromptOverrideException>(async () => await BflPromptLibrary.LoadPartsAsync(BflStep.FRAMEWORK, AddressForm.FORMAL, this.directory));
var inOnboarding = await Assert.ThrowsAsync<BflPromptOverrideException>(async () => await BflPromptLibrary.LoadOnboardingPartsAsync(AddressForm.FORMAL, this.directory));
using (Assert.EnterMultipleScope())
{
Assert.That(inStep!.FileName, Is.EqualTo($"Prompts/steps/{BflStep.STRATEGY.FolderName}.md"));
Assert.That(inStep.Placeholder, Is.EqualTo(BflPromptTemplate.DETAILS));
Assert.That(inCore!.FileName, Is.EqualTo("Prompts/core.md"));
Assert.That(inCore.Placeholder, Is.EqualTo("UNKNOWN"));
Assert.That(inOnboarding!.FileName, Is.EqualTo("Prompts/onboarding.md"));
Assert.That(inOnboarding.Placeholder, Is.EqualTo(BflPromptTemplate.KNOWLEDGE));
}
}
[Test]
public async Task AFileWithThePlaceholdersOfItsPromptGivesAPrompt()
{
await this.WriteAsync(BflPromptLibrary.CORE_PATH, "Ein neuer Kern.\n{{RESPONSE_LANGUAGE}}\n{{ADDRESS_FORM}}\n{{KNOWLEDGE}}\n{{STEP}}\n{{MATERIALS}}\n{{DETAILS}}\n{{MISSING_DETAILS}}");
await this.WriteAsync(BflPromptLibrary.ONBOARDING_PATH, "Ein neues Onboarding.\n{{RESPONSE_LANGUAGE}}\n{{ADDRESS_FORM}}");
var prompt = BflPromptComposer.Compose(await BflPromptLibrary.LoadPartsAsync(BflStep.TEST_SITUATION, AddressForm.FORMAL, this.directory), BflStep.TEST_SITUATION, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch");
var onboarding = BflPromptComposer.ComposeOnboarding(await BflPromptLibrary.LoadOnboardingPartsAsync(AddressForm.FORMAL, this.directory), "Deutsch");
using (Assert.EnterMultipleScope())
{
Assert.That(prompt, Does.StartWith("Ein neuer Kern."));
Assert.That(prompt, Does.Not.Contain("{{"));
Assert.That(onboarding, Does.StartWith("Ein neues Onboarding."));
Assert.That(onboarding, Does.Not.Contain("{{"));
}
}
[Test]
public async Task CopyingGivesTheFolderEveryPromptOfThisVersion()
{
var copied = await BflPromptOverrides.CopyBuiltInAsync(this.directory);
using (Assert.EnterMultipleScope())
{
Assert.That(copied, Is.EqualTo(BflPromptLibrary.AllPaths.Count));
foreach (var path in BflPromptLibrary.AllPaths)
Assert.That(await File.ReadAllTextAsync(BflPromptOverrides.FilePath(this.directory, path)), Is.EqualTo(await BflPromptLibrary.ReadBuiltInAsync(path)), path);
Assert.That(await BflPromptOverrides.FindAsync(this.directory), Is.Empty, "A copy which nobody changed replaces nothing.");
Assert.That(Directory.EnumerateFiles(this.directory, "*.tmp", SearchOption.AllDirectories), Is.Empty);
}
}
[Test]
public async Task CopyingKeepsEveryFileWhichExists()
{
await this.WriteAsync(BflPromptLibrary.CORE_PATH, "Ein neuer Kern.");
await this.WriteAsync(BflPromptLibrary.StepPath(BflStep.RESULTS), string.Empty);
var copied = await BflPromptOverrides.CopyBuiltInAsync(this.directory);
var copiedAgain = await BflPromptOverrides.CopyBuiltInAsync(this.directory);
using (Assert.EnterMultipleScope())
{
Assert.That(copied, Is.EqualTo(BflPromptLibrary.AllPaths.Count - 2));
Assert.That(copiedAgain, Is.Zero);
Assert.That(await File.ReadAllTextAsync(BflPromptOverrides.FilePath(this.directory, BflPromptLibrary.CORE_PATH)), Is.EqualTo("Ein neuer Kern."));
Assert.That(await File.ReadAllTextAsync(BflPromptOverrides.FilePath(this.directory, BflPromptLibrary.StepPath(BflStep.RESULTS))), Is.Empty, "Even an empty file is the person's own.");
}
}
[Test]
public async Task TheLabTellsAboutReplacedPromptsOnlyWhileTheSettingAllowsThem()
{
var settingsManager = new SettingsManager(NullLogger<SettingsManager>.Instance, null!);
var store = new BeyondFactsLabStore(NullLogger<BeyondFactsLabStore>.Instance, new BflStorageOptions { DataDirectory = this.directory });
var service = new BflConversationService(store, settingsManager, null!, null!, NullLogger<BflConversationService>.Instance);
await BflPromptOverrides.CopyBuiltInAsync(store.PromptOverridesDirectory);
settingsManager.ConfigurationData.BeyondFactsLab.AllowPromptOverrides = true;
var withCopies = await service.UsesPromptOverridesAsync();
await File.WriteAllTextAsync(BflPromptOverrides.FilePath(store.PromptOverridesDirectory, BflPromptLibrary.CORE_PATH), "Ein neuer Kern.");
var withChange = await service.UsesPromptOverridesAsync();
settingsManager.ConfigurationData.BeyondFactsLab.AllowPromptOverrides = false;
var switchedOff = await service.UsesPromptOverridesAsync();
using (Assert.EnterMultipleScope())
{
Assert.That(store.PromptOverridesDirectory, Is.EqualTo(Path.Combine(this.directory, BeyondFactsLabStore.ROOT_DIRECTORY_NAME, BflPromptOverrides.DIRECTORY_NAME)));
Assert.That(withCopies, Is.False, "Copies which nobody changed replace nothing.");
Assert.That(withChange, Is.True);
Assert.That(switchedOff, Is.False);
}
}
[Test]
public async Task ACopyWithOtherLineBreaksReplacesNothing()
{
var path = BflPromptLibrary.StepPath(BflStep.FRAMEWORK);
var builtIn = await BflPromptLibrary.ReadBuiltInAsync(path);
await this.WriteAsync(path, builtIn.ReplaceLineEndings("\r\n") + "\r\n");
Assert.That(await BflPromptOverrides.FindAsync(this.directory), Is.Empty);
}
/// <summary>
/// Writes the file which replaces a prompt.
/// </summary>
/// <param name="path">The prompt, one of BflPromptLibrary.AllPaths.</param>
/// <param name="text">The text of the file.</param>
private async Task WriteAsync(string path, string text)
{
var file = BflPromptOverrides.FilePath(this.directory, path);
Directory.CreateDirectory(Path.GetDirectoryName(file)!);
await File.WriteAllTextAsync(file, text);
}
}