mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-10 20:07:21 +00:00
333 lines
16 KiB
C#
333 lines
16 KiB
C#
using AIStudio.Assistants.BeyondFactsLab;
|
|
using AIStudio.Settings.DataModel;
|
|
|
|
namespace AIStudio.Tests.BeyondFactsLab;
|
|
|
|
/// <summary>
|
|
/// Checks how the BeyondFacts Lab puts together the system prompt of a step.
|
|
/// </summary>
|
|
[TestFixture]
|
|
public sealed class BflPromptComposerTests
|
|
{
|
|
private const string CORE = "Sprache: {{RESPONSE_LANGUAGE}}\n{{ADDRESS_FORM}}\n{{KNOWLEDGE}}\n{{STEP}}\n{{MATERIALS}}\n{{DETAILS}}\n{{MISSING_DETAILS}}";
|
|
|
|
private static readonly BflPromptParts PARTS = new(CORE, "Leitfaden", "Schritt", "Anrede");
|
|
|
|
[Test]
|
|
public void APlaceholderWithoutAValueIsAnError()
|
|
{
|
|
var exception = Assert.Throws<FormatException>(() => BflPromptTemplate.Fill("Hallo {{UNKNOWN}}", new Dictionary<string, string>()));
|
|
|
|
Assert.That(exception!.Message, Does.Contain("UNKNOWN"));
|
|
}
|
|
|
|
[Test]
|
|
public void AMisspelledPlaceholderIsAnError()
|
|
{
|
|
Assert.Throws<FormatException>(() => BflPromptTemplate.Fill("Hallo {{ step }}", new Dictionary<string, string> { [BflPromptTemplate.STEP] = "Schritt" }));
|
|
}
|
|
|
|
[Test]
|
|
public void AValueIsInsertedAsItIs()
|
|
{
|
|
var prompt = BflPromptTemplate.Fill("{{STEP}}", new Dictionary<string, string> { [BflPromptTemplate.STEP] = "Die Person schrieb {{KNOWLEDGE}}." });
|
|
|
|
Assert.That(prompt, Is.EqualTo("Die Person schrieb {{KNOWLEDGE}}."), "What the person entered must never be read as a placeholder.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task ThePromptOfEveryStepFillsEveryPlaceholder()
|
|
{
|
|
foreach (var step in BflStep.All)
|
|
{
|
|
var parts = await BflPromptLibrary.LoadPartsAsync(step, AddressForm.FORMAL);
|
|
var prompt = BflPromptComposer.Compose(parts, step, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Not.Contain("{{"), step.ToString());
|
|
Assert.That(prompt, Does.Contain(parts.Step.Trim()), step.ToString());
|
|
Assert.That(prompt, Does.Contain(parts.Knowledge.Trim()[..200]), step.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task TheFormOfAddressDecidesTheInstruction()
|
|
{
|
|
var formal = BflPromptComposer.Compose(await BflPromptLibrary.LoadPartsAsync(BflStep.FRAMEWORK, AddressForm.FORMAL), BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch");
|
|
var informal = BflPromptComposer.Compose(await BflPromptLibrary.LoadPartsAsync(BflStep.FRAMEWORK, AddressForm.INFORMAL), BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(formal, Does.Contain("mit „Sie“"));
|
|
Assert.That(formal, Does.Not.Contain("mit „du“"));
|
|
Assert.That(informal, Does.Contain("mit „du“"));
|
|
Assert.That(informal, Does.Not.Contain("mit „Sie“"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task TheOnboardingFillsEveryPlaceholderWithoutTheGuide()
|
|
{
|
|
var parts = await BflPromptLibrary.LoadOnboardingPartsAsync(AddressForm.FORMAL);
|
|
var knowledge = await BflPromptLibrary.ReadAsync(BflPromptLibrary.KNOWLEDGE_PATH);
|
|
|
|
var prompt = BflPromptComposer.ComposeOnboarding(parts, "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Not.Contain("{{"));
|
|
Assert.That(prompt, Does.Contain("Antworte auf Deutsch."));
|
|
Assert.That(prompt, Does.Not.Contain(knowledge.Trim()[..200]), "The onboarding stays short, so it leaves out the guide.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task TheFormOfAddressDecidesTheInstructionOfTheOnboarding()
|
|
{
|
|
var formal = BflPromptComposer.ComposeOnboarding(await BflPromptLibrary.LoadOnboardingPartsAsync(AddressForm.FORMAL), "Deutsch");
|
|
var informal = BflPromptComposer.ComposeOnboarding(await BflPromptLibrary.LoadOnboardingPartsAsync(AddressForm.INFORMAL), "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(formal, Does.Contain("mit „Sie“"));
|
|
Assert.That(formal, Does.Not.Contain("mit „du“"));
|
|
Assert.That(informal, Does.Contain("mit „du“"));
|
|
Assert.That(informal, Does.Not.Contain("mit „Sie“"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TheOnboardingIsTheSameOnEverySystem()
|
|
{
|
|
var parts = new BflOnboardingPromptParts("Sprache: {{RESPONSE_LANGUAGE}}\r\n{{ADDRESS_FORM}}\r\nEnde", " Anrede\r\n");
|
|
|
|
var prompt = BflPromptComposer.ComposeOnboarding(parts, "Deutsch");
|
|
|
|
Assert.That(prompt, Is.EqualTo("Sprache: Deutsch\nAnrede\nEnde"));
|
|
}
|
|
|
|
[Test]
|
|
public void TheOnboardingKnowsNoDetailsOfAPreparation()
|
|
{
|
|
var parts = new BflOnboardingPromptParts("{{DETAILS}}", "Anrede");
|
|
|
|
Assert.Throws<FormatException>(() => BflPromptComposer.ComposeOnboarding(parts, "Deutsch"));
|
|
}
|
|
|
|
[Test]
|
|
public void TheLanguageOfTheAnswersIsNamed()
|
|
{
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "English (United States)");
|
|
|
|
Assert.That(prompt, Does.StartWith("Sprache: English (United States)\n"));
|
|
}
|
|
|
|
[Test]
|
|
public void TheMissingKeyDetailsAreListed()
|
|
{
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch");
|
|
|
|
foreach (var field in BflFieldCatalog.FIELDS.Where(field => field.IsKeyDetail))
|
|
Assert.That(prompt, Does.Contain(field.Label), field.Id);
|
|
}
|
|
|
|
[Test]
|
|
public void NothingIsMissingWhenEveryKeyDetailIsThere()
|
|
{
|
|
var forms = BflStep.All.ToDictionary(step => step, _ => new BflStepFormData());
|
|
foreach (var field in BflFieldCatalog.FIELDS.Where(field => field.IsKeyDetail))
|
|
forms[field.Step].Set(field.Id, "vorhanden");
|
|
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, forms, [], "Deutsch");
|
|
|
|
Assert.That(prompt, Does.EndWith("Es fehlen keine wichtigen Angaben."));
|
|
}
|
|
|
|
[Test]
|
|
public void TheDetailsShowTheLabelsAndTheChosenOptions()
|
|
{
|
|
var framework = new BflStepFormData();
|
|
framework.Set(BflFieldCatalog.FORMAT, "Podiumsdiskussion");
|
|
var goals = new BflStepFormData();
|
|
var directions = BflFieldCatalog.FIELDS.Single(field => field.Id == BflFieldCatalog.GOAL_DIRECTIONS);
|
|
goals.SetChoices(directions, [BflFieldCatalog.GOAL_INFORM, BflFieldCatalog.GOAL_SHOW_OPTIONS]);
|
|
var forms = new Dictionary<BflStep, BflStepFormData> { [BflStep.FRAMEWORK] = framework, [BflStep.GOALS_AND_ROLE] = goals };
|
|
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, forms, [], "Deutsch");
|
|
var inform = directions.Options.Single(option => option.Id == BflFieldCatalog.GOAL_INFORM).Label;
|
|
var showOptions = directions.Options.Single(option => option.Id == BflFieldCatalog.GOAL_SHOW_OPTIONS).Label;
|
|
var format = BflFieldCatalog.FIELDS.Single(field => field.Id == BflFieldCatalog.FORMAT).Label;
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Contain($"- {format}: Podiumsdiskussion"));
|
|
Assert.That(prompt, Does.Contain($"- {directions.Label}: {inform}, {showOptions}"));
|
|
Assert.That(prompt, Does.Contain($"## {BflStep.FRAMEWORK.Title} (dieser Schritt)"));
|
|
Assert.That(prompt, Does.Not.Contain(BflFieldCatalog.GOAL_INFORM), "The model reads the labels, not the IDs.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ALongTextKeepsItsLines()
|
|
{
|
|
var knowledge = new BflStepFormData();
|
|
knowledge.Set(BflFieldCatalog.CORE_THESES, "Erste These\r\nZweite These");
|
|
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.KNOWLEDGE, new Dictionary<BflStep, BflStepFormData> { [BflStep.KNOWLEDGE] = knowledge }, [], "Deutsch");
|
|
|
|
Assert.That(prompt, Does.Contain(": Erste These\n Zweite These"));
|
|
}
|
|
|
|
[Test]
|
|
public void APersonalDetailReachesOnlyTheChatOfItsOwnStep()
|
|
{
|
|
var values = new BflStepFormData();
|
|
values.Set(BflFieldCatalog.PERSONAL_CONCERN, "VERTRAULICH-4711");
|
|
values.Set(BflFieldCatalog.VALUES_AND_CONCERNS, "Offenheit");
|
|
var forms = new Dictionary<BflStep, BflStepFormData> { [BflStep.VALUES_AND_PERSPECTIVES] = values };
|
|
|
|
foreach (var step in BflStep.All)
|
|
{
|
|
var prompt = BflPromptComposer.Compose(PARTS, step, forms, [], "Deutsch");
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt.Contains("VERTRAULICH-4711", StringComparison.Ordinal), Is.EqualTo(step is BflStep.VALUES_AND_PERSPECTIVES), step.ToString());
|
|
Assert.That(prompt, Does.Contain("Offenheit"), "Only what the person confided stays in its step.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TheSameFormsGiveTheSamePrompt()
|
|
{
|
|
var framework = new BflStepFormData();
|
|
framework.Set(BflFieldCatalog.AUDIENCE, "Öffentlichkeit");
|
|
framework.Set(BflFieldCatalog.FORMAT, "Interview");
|
|
var strategy = new BflStepFormData();
|
|
strategy.Set(BflFieldCatalog.COMMUNICATION_WORRIES, "Zeitdruck");
|
|
|
|
var first = BflPromptComposer.Compose(PARTS, BflStep.STRATEGY, new Dictionary<BflStep, BflStepFormData> { [BflStep.FRAMEWORK] = framework, [BflStep.STRATEGY] = strategy }, [], "Deutsch");
|
|
var second = BflPromptComposer.Compose(PARTS, BflStep.STRATEGY, new Dictionary<BflStep, BflStepFormData> { [BflStep.STRATEGY] = strategy, [BflStep.FRAMEWORK] = framework }, [], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(second, Is.EqualTo(first));
|
|
Assert.That(first, Does.Not.Contain("\r"));
|
|
Assert.That(first.IndexOf("Interview", StringComparison.Ordinal), Is.LessThan(first.IndexOf("Öffentlichkeit", StringComparison.Ordinal)), "The details follow the order of the forms.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void APreparationWithoutMaterialSaysSo()
|
|
{
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [], "Deutsch");
|
|
|
|
Assert.That(prompt, Does.Contain("Die Person hat kein Material hinzugefügt."));
|
|
}
|
|
|
|
[Test]
|
|
public void TheMaterialComesWithItsNameAndText()
|
|
{
|
|
var invitation = Excerpt("Einladung.docx", "Podium am 10. Dezember\r\nim Audimax", 10_000);
|
|
var paper = Excerpt("Paper.pdf", "Abstract", 10_000);
|
|
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [invitation, paper], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Contain("## Material 1: „Einladung.docx“\n\nPodium am 10. Dezember\nim Audimax"));
|
|
Assert.That(prompt, Does.Contain("## Material 2: „Paper.pdf“\n\nAbstract"));
|
|
Assert.That(prompt, Does.Not.Contain("Gekürzt"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void AShortenedMaterialSaysHowMuchOfItIsThere()
|
|
{
|
|
var material = new BflMaterial { MaterialId = Guid.NewGuid(), Name = "Paper.pdf", TextLength = 1_000 };
|
|
var shortened = new BflMaterialExcerpt(new(material, 400), "Der Anfang");
|
|
var missing = new BflMaterialExcerpt(new(new BflMaterial { MaterialId = Guid.NewGuid(), Name = "Buch.pdf", TextLength = 1_000 }, 0), string.Empty);
|
|
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [shortened, missing], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Contain("(Gekürzt: Hier steht nur der Anfang, etwa 40 % des Materials."));
|
|
Assert.That(prompt, Does.Contain("## Material 2: „Buch.pdf“\n(Für dieses Material reicht der Platz im Kontext des Modells nicht. Du kennst nur seinen Namen.)"));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void APlaceholderInTheMaterialStaysText()
|
|
{
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [Excerpt("Notizen.md", "Siehe {{KNOWLEDGE}}", 100)], "Deutsch");
|
|
|
|
Assert.That(prompt, Does.Contain("Siehe {{KNOWLEDGE}}"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task TheMaterialStandsBetweenTheStepAndTheDetails()
|
|
{
|
|
var parts = await BflPromptLibrary.LoadPartsAsync(BflStep.KNOWLEDGE, AddressForm.FORMAL);
|
|
var knowledge = new BflStepFormData();
|
|
knowledge.Set(BflFieldCatalog.CORE_THESES, "THESE-4711");
|
|
|
|
var prompt = BflPromptComposer.Compose(parts, BflStep.KNOWLEDGE, new Dictionary<BflStep, BflStepFormData> { [BflStep.KNOWLEDGE] = knowledge }, [Excerpt("Paper.pdf", "MATERIAL-4711", 100)], "Deutsch");
|
|
var material = prompt.IndexOf("MATERIAL-4711", StringComparison.Ordinal);
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt.IndexOf(parts.Step.Trim(), StringComparison.Ordinal), Is.LessThan(material), "The material changes less often than the details, so it comes first for the prompt caching.");
|
|
Assert.That(material, Is.LessThan(prompt.IndexOf("THESE-4711", StringComparison.Ordinal)));
|
|
}
|
|
}
|
|
|
|
[TestCase(BflImageDelivery.SENT, "(Ein Bild. Es liegt der ersten Nachricht dieses Gesprächs bei.)")]
|
|
[TestCase(BflImageDelivery.MODEL_SEES_NO_IMAGES, "(Ein Bild. Das Modell kann keine Bilder sehen. Du kennst nur seinen Namen.)")]
|
|
[TestCase(BflImageDelivery.OVER_THE_LIMIT, "(Ein Bild. Das Modell nimmt nicht so viele Bilder auf einmal an. Du kennst nur seinen Namen.)")]
|
|
public void AnImageSaysWhetherItGoesAlong(BflImageDelivery delivery, string expected)
|
|
{
|
|
var image = new BflMaterial { MaterialId = Guid.NewGuid(), Name = "Poster.png", Kind = BflMaterialKind.IMAGE };
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [new(new(image, 0, delivery), string.Empty)], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Contain($"## Material 1: „Poster.png“\n{expected}"));
|
|
Assert.That(prompt, Does.Not.Contain("reicht der Platz"), "An image takes no room in the prompt.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void ARecordingIsMarkedAsATranscript()
|
|
{
|
|
var recording = new BflMaterial { MaterialId = Guid.NewGuid(), Name = "Interview.mp3", Kind = BflMaterialKind.RECORDING, TextLength = 10 };
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [new(new(recording, 10), "Guten Tag.")], "Deutsch");
|
|
|
|
Assert.That(prompt, Does.Contain("## Material 1: „Interview.mp3“\n(Das Transkript einer Aufnahme."));
|
|
}
|
|
|
|
[Test]
|
|
public void AMaterialWhoseEndTheLabLeftOutSaysSo()
|
|
{
|
|
var book = new BflMaterial { MaterialId = Guid.NewGuid(), Name = "Buch.pdf", TextLength = 10, IsShortened = true };
|
|
var prompt = BflPromptComposer.Compose(PARTS, BflStep.FRAMEWORK, new Dictionary<BflStep, BflStepFormData>(), [new(new(book, 10), "Kapitel 1.")], "Deutsch");
|
|
|
|
using (Assert.EnterMultipleScope())
|
|
{
|
|
Assert.That(prompt, Does.Contain("(Das Lab hat nur den Anfang dieses Materials übernommen. Den Rest kennst du nicht.)"));
|
|
Assert.That(prompt, Does.Not.Contain("Gekürzt:"), "The whole stored text fits.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void TheOnboardingKnowsNoMaterial()
|
|
{
|
|
var parts = new BflOnboardingPromptParts("{{MATERIALS}}", "Anrede");
|
|
|
|
Assert.Throws<FormatException>(() => BflPromptComposer.ComposeOnboarding(parts, "Deutsch"));
|
|
}
|
|
|
|
private static BflMaterialExcerpt Excerpt(string name, string text, int textLength) => new(new(new BflMaterial { MaterialId = Guid.NewGuid(), Name = name, TextLength = textLength }, textLength), text);
|
|
} |