using System.Collections.Frozen;
using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BeyondFactsLab;
///
/// Reads the prompts and the knowledge base of the BeyondFacts Lab, which come with AI Studio.
///
///
/// 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.
/// While the person allows it, files in the folder for prompt overrides replace them, see BflPromptOverrides.
///
public static class BflPromptLibrary
{
///
/// The folder of the lab, relative to the project folder.
///
private const string FOLDER = "Assistants/BeyondFactsLab";
///
/// The prompt every chat of the lab starts with: role, principles, and the places of the other parts.
///
public const string CORE_PATH = FOLDER + "/Prompts/core.md";
///
/// The guide by Lara Wüster (TUM), which every chat of the lab keeps in its context.
///
public const string KNOWLEDGE_PATH = FOLDER + "/Knowledge/beyondfacts-paper.md";
///
/// The prompt of the conversation in which the lab tells whether it fits a task. It stands on its own, without
/// the core and the guide.
///
public const string ONBOARDING_PATH = FOLDER + "/Prompts/onboarding.md";
///
/// The request which lets the model write the work state of a step, at the end of the conversation of the step.
///
public const string WORK_STATE_PATH = FOLDER + "/Prompts/work-state.md";
///
/// The folder of the requests which let the model write a result, one for each kind a model writes.
///
private const string RESULTS_FOLDER = FOLDER + "/Prompts/results/";
///
/// The instruction to address the person formally.
///
private const string ADDRESS_FORMAL_PATH = FOLDER + "/Prompts/snippets/address-formal.md";
///
/// The instruction to address the person informally.
///
private const string ADDRESS_INFORMAL_PATH = FOLDER + "/Prompts/snippets/address-informal.md";
///
/// The placeholders BflPromptComposer fills in the core.
///
private static readonly FrozenSet CORE_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal,
BflPromptTemplate.RESPONSE_LANGUAGE,
BflPromptTemplate.ADDRESS_FORM,
BflPromptTemplate.KNOWLEDGE,
BflPromptTemplate.STEP,
BflPromptTemplate.MATERIALS,
BflPromptTemplate.DETAILS,
BflPromptTemplate.MISSING_DETAILS,
BflPromptTemplate.WORK_STATES);
///
/// The placeholders BflPromptComposer fills in the prompt of the onboarding.
///
private static readonly FrozenSet ONBOARDING_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal,
BflPromptTemplate.RESPONSE_LANGUAGE,
BflPromptTemplate.ADDRESS_FORM);
///
/// The placeholders BflPromptComposer fills in the request for a work state.
///
private static readonly FrozenSet WORK_STATE_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal,
BflPromptTemplate.SECTIONS,
BflPromptTemplate.FIELDS);
///
/// The placeholders BflPromptComposer fills in the request for a result.
///
private static readonly FrozenSet RESULT_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal,
BflPromptTemplate.MISSING_WORK_STATES);
///
/// The placeholders of every other file: none, because BflPromptComposer inserts these files as values.
///
private static readonly FrozenSet NO_PLACEHOLDERS = FrozenSet.Empty;
///
/// Gets the prompt of a step, named like the folder of the step.
///
/// The step.
public static string StepPath(BflStep step) => $"{FOLDER}/Prompts/steps/{step.FolderName}.md";
///
/// Gets the request which lets the model write a result.
///
/// The kind of result, one a model writes.
/// No model writes this kind of result.
public static string ResultPath(BflResultKind kind) => kind switch
{
BflResultKind.POSITION_PAPER => RESULTS_FOLDER + "position-paper.md",
BflResultKind.CONVERSATION_GUIDE => RESULTS_FOLDER + "conversation-guide.md",
BflResultKind.QUESTIONS_AND_ANSWERS => RESULTS_FOLDER + "questions-and-answers.md",
BflResultKind.OPEN_POINTS => RESULTS_FOLDER + "open-points.md",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "No model writes this result.")
};
///
/// Gets the instruction for a form of address.
///
/// The form of address.
public static string AddressPath(AddressForm addressForm) => addressForm is AddressForm.INFORMAL ? ADDRESS_INFORMAL_PATH : ADDRESS_FORMAL_PATH;
///
/// Gets every file of the lab's prompts, e.g., to check that the app comes with all of them.
///
public static IReadOnlyList AllPaths =>
[
CORE_PATH,
KNOWLEDGE_PATH,
ONBOARDING_PATH,
WORK_STATE_PATH,
..BflResultKind.All.Where(kind => kind.IsWrittenByModel).Select(ResultPath),
..Enum.GetValues().Select(AddressPath).Distinct(),
..BflStep.All.Select(StepPath)
];
///
/// Gets a file of the lab's prompts relative to the folder of the lab, e.g., "Prompts/core.md".
///
/// The file, one of the paths of this class.
/// The file lies outside the folder of the lab.
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));
///
/// Gets the placeholders a file of the lab's prompts may contain.
///
///
/// BflPromptComposer fills placeholders in the core, in the prompt of the onboarding, and in the requests for a work
/// state and for a result 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.
///
/// The file, one of the paths of this class.
public static IReadOnlySet PlaceholdersOf(string path) => path switch
{
CORE_PATH => CORE_PLACEHOLDERS,
ONBOARDING_PATH => ONBOARDING_PLACEHOLDERS,
WORK_STATE_PATH => WORK_STATE_PLACEHOLDERS,
_ when path.StartsWith(RESULTS_FOLDER, StringComparison.Ordinal) => RESULT_PLACEHOLDERS,
_ => NO_PLACEHOLDERS
};
///
/// Reads the texts of the system prompt of a step.
///
/// The step.
/// How the model addresses the person.
/// The folder whose files replace the prompts, or null while the person does not allow it.
/// The cancellation token.
/// The texts, which BflPromptComposer puts together.
/// The app comes without one of the files.
/// A file which replaces a prompt contains a placeholder which the lab does not fill in it.
public static async Task LoadPartsAsync(BflStep step, AddressForm addressForm, string? overrideDirectory = null, CancellationToken token = default) => new(
await ReadAsync(CORE_PATH, overrideDirectory, token),
await ReadAsync(KNOWLEDGE_PATH, overrideDirectory, token),
await ReadAsync(StepPath(step), overrideDirectory, token),
await ReadAsync(AddressPath(addressForm), overrideDirectory, token));
///
/// Reads the texts of the system prompt of the conversation about whether the lab fits a task.
///
/// How the model addresses the person.
/// The folder whose files replace the prompts, or null while the person does not allow it.
/// The cancellation token.
/// The texts, which BflPromptComposer puts together.
/// The app comes without one of the files.
/// A file which replaces a prompt contains a placeholder which the lab does not fill in it.
public static async Task LoadOnboardingPartsAsync(AddressForm addressForm, string? overrideDirectory = null, CancellationToken token = default) => new(
await ReadAsync(ONBOARDING_PATH, overrideDirectory, token),
await ReadAsync(AddressPath(addressForm), overrideDirectory, token));
///
/// 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.
///
/// The file, one of the paths of this class.
/// The folder whose files replace the prompts, or null while the person does not allow it.
/// The cancellation token.
/// The text of the file.
/// The app comes without the file, or the file is empty.
/// The file which replaces the prompt contains a placeholder which the lab does not fill in it.
public static async Task 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);
}
///
/// Reads a file of the lab's prompts as it comes with the app.
///
/// The file, one of the paths of this class.
/// The cancellation token.
/// The text of the file.
/// The app comes without the file, or the file is empty.
public static async Task ReadBuiltInAsync(string path, CancellationToken token = default)
{
var text = await AppResources.ReadTextAsync(path, token);
if (string.IsNullOrWhiteSpace(text))
throw new FileNotFoundException("The BeyondFacts Lab misses one of its prompts.", path);
return text;
}
}