2026-11-10 08:57:36 +01:00
using System.Collections.Frozen ;
2026-11-09 09:55:01 +01:00
using AIStudio.Settings.DataModel ;
namespace AIStudio.Assistants.BeyondFactsLab ;
/// <summary>
/// Reads the prompts and the knowledge base of the BeyondFacts Lab, which come with AI Studio.
/// </summary>
/// <remarks>
/// 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.
2026-11-10 08:57:36 +01:00
/// While the person allows it, files in the folder for prompt overrides replace them, see BflPromptOverrides.
2026-11-09 09:55:01 +01:00
/// </remarks>
public static class BflPromptLibrary
{
/// <summary>
/// The folder of the lab, relative to the project folder.
/// </summary>
private const string FOLDER = "Assistants/BeyondFactsLab" ;
/// <summary>
/// The prompt every chat of the lab starts with: role, principles, and the places of the other parts.
/// </summary>
public const string CORE_PATH = FOLDER + "/Prompts/core.md" ;
/// <summary>
/// The guide by Lara Wüster (TUM), which every chat of the lab keeps in its context.
/// </summary>
public const string KNOWLEDGE_PATH = FOLDER + "/Knowledge/beyondfacts-paper.md" ;
2026-11-09 12:42:59 +01:00
/// <summary>
/// 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.
/// </summary>
public const string ONBOARDING_PATH = FOLDER + "/Prompts/onboarding.md" ;
2026-11-09 09:55:01 +01:00
/// <summary>
/// The instruction to address the person formally.
/// </summary>
private const string ADDRESS_FORMAL_PATH = FOLDER + "/Prompts/snippets/address-formal.md" ;
/// <summary>
/// The instruction to address the person informally.
/// </summary>
private const string ADDRESS_INFORMAL_PATH = FOLDER + "/Prompts/snippets/address-informal.md" ;
2026-11-10 08:57:36 +01:00
/// <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 ;
2026-11-09 09:55:01 +01:00
/// <summary>
/// Gets the prompt of a step, named like the folder of the step.
/// </summary>
/// <param name="step">The step.</param>
public static string StepPath ( BflStep step ) => $"{FOLDER}/Prompts/steps/{step.FolderName}.md" ;
/// <summary>
/// Gets the instruction for a form of address.
/// </summary>
/// <param name="addressForm">The form of address.</param>
public static string AddressPath ( AddressForm addressForm ) => addressForm is AddressForm . INFORMAL ? ADDRESS_INFORMAL_PATH : ADDRESS_FORMAL_PATH ;
/// <summary>
/// Gets every file of the lab's prompts, e.g., to check that the app comes with all of them.
/// </summary>
public static IReadOnlyList < string > AllPaths =>
[
CORE_PATH,
KNOWLEDGE_PATH,
2026-11-09 12:42:59 +01:00
ONBOARDING_PATH,
2026-11-09 09:55:01 +01:00
..Enum.GetValues<AddressForm>().Select(AddressPath).Distinct(),
..BflStep.All.Select(StepPath)
] ;
2026-11-10 08:57:36 +01:00
/// <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
};
2026-11-09 09:55:01 +01:00
/// <summary>
/// Reads the texts of the system prompt of a step.
/// </summary>
/// <param name="step">The step.</param>
/// <param name="addressForm">How the model addresses the person.</param>
2026-11-10 08:57:36 +01:00
/// <param name="overrideDirectory">The folder whose files replace the prompts, or null while the person does not allow it.</param>
2026-11-09 09:55:01 +01:00
/// <param name="token">The cancellation token.</param>
/// <returns>The texts, which BflPromptComposer puts together.</returns>
/// <exception cref="FileNotFoundException">The app comes without one of the files.</exception>
2026-11-10 08:57:36 +01:00
/// <exception cref="BflPromptOverrideException">A file which replaces a prompt contains a placeholder which the lab does not fill in it.</exception>
public static async Task < BflPromptParts > 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 ));
2026-11-09 09:55:01 +01:00
2026-11-09 12:42:59 +01:00
/// <summary>
/// Reads the texts of the system prompt of the conversation about whether the lab fits a task.
/// </summary>
/// <param name="addressForm">How the model addresses the person.</param>
2026-11-10 08:57:36 +01:00
/// <param name="overrideDirectory">The folder whose files replace the prompts, or null while the person does not allow it.</param>
2026-11-09 12:42:59 +01:00
/// <param name="token">The cancellation token.</param>
/// <returns>The texts, which BflPromptComposer puts together.</returns>
/// <exception cref="FileNotFoundException">The app comes without one of the files.</exception>
2026-11-10 08:57:36 +01:00
/// <exception cref="BflPromptOverrideException">A file which replaces a prompt contains a placeholder which the lab does not fill in it.</exception>
public static async Task < BflOnboardingPromptParts > LoadOnboardingPartsAsync ( AddressForm addressForm , string? overrideDirectory = null , CancellationToken token = default ) => new (
await ReadAsync ( ONBOARDING_PATH , overrideDirectory , token ),
await ReadAsync ( AddressPath ( addressForm ), overrideDirectory , token ));
/// <summary>
/// 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 );
}
2026-11-09 12:42:59 +01:00
2026-11-09 09:55:01 +01:00
/// <summary>
2026-11-10 08:57:36 +01:00
/// Reads a file of the lab's prompts as it comes with the app.
2026-11-09 09:55:01 +01:00
/// </summary>
/// <param name="path">The file, one of the paths of this class.</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>
2026-11-10 08:57:36 +01:00
public static async Task < string > ReadBuiltInAsync ( string path , CancellationToken token = default )
2026-11-09 09:55:01 +01:00
{
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 ;
}
}