using System.Text;
using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BeyondFactsLab;
///
/// 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.
///
///
/// 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.
///
public static class BflPromptOverrides
{
///
/// The name of the folder below the folder of the lab.
///
public const string DIRECTORY_NAME = "promptOverrides";
///
/// Gets the folder whose files replace the prompts of the lab, or null while the setting does not allow them.
///
/// The settings of the lab, which an organization may lock.
/// The folder for prompt overrides.
public static string? ActiveDirectory(DataBeyondFactsLab settings, string directory) => settings.AllowPromptOverrides ? directory : null;
///
/// Reads the file which replaces a prompt of the lab, and checks its placeholders.
///
/// The folder for prompt overrides.
/// The prompt, one of BflPromptLibrary.AllPaths.
/// The cancellation token.
/// The text of the file, or null when no file replaces the prompt.
/// The file contains a placeholder which the lab does not fill in it.
public static async Task 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;
}
///
/// Finds the prompts of the lab which a file in the folder replaces with another text.
///
///
/// 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.
///
/// The folder for prompt overrides.
/// The cancellation token.
/// The prompts, as paths of BflPromptLibrary.AllPaths.
public static async Task> FindAsync(string directory, CancellationToken token = default)
{
var replaced = new List();
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;
}
///
/// Copies the prompts of this version into the folder, so that changes start from them. A file which exists stays
/// as it is.
///
/// The folder for prompt overrides; it is created when it is missing.
/// The cancellation token.
/// How many files the folder got.
public static async Task 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;
}
///
/// Gets the file which replaces a prompt of the lab.
///
/// The folder for prompt overrides.
/// The prompt, one of BflPromptLibrary.AllPaths.
public static string FilePath(string directory, string path) => Path.Join([directory, ..BflPromptLibrary.RelativePath(path).Split('/')]);
///
/// Reads the file which replaces a prompt of the lab, without checking it.
///
/// The folder for prompt overrides.
/// The prompt, one of BflPromptLibrary.AllPaths.
/// The cancellation token.
/// The text of the file, or null when there is no such file or it is empty.
private static async Task 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;
}
///
/// Brings a prompt into the form in which two copies compare, whatever system wrote them.
///
/// The prompt.
private static string Normalize(string text) => text.ReplaceLineEndings("\n").Trim();
}