Added the choice of simulated participants to the test situation of the BeyondFacts Lab

This commit is contained in:
Thorsten Sommer committed 2026-11-15 16:17:41 +01:00
1 parent 92d0ff68f4
commit cbe6f30e53
13 files changed
+557 -5

No files matched your search

@@ -113,12 +113,19 @@ public sealed partial class BeyondFactsLabStore
throw new InvalidDataException("This version of AI Studio cannot open the form.", exception); throw new InvalidDataException("This version of AI Studio cannot open the form.", exception);
} }
if (form is null || form.SchemaVersion is < 1 or > BflStepFormData.CURRENT_SCHEMA_VERSION || IsNull(form.Values) || form.CardSort is { } cardSort && IsBroken(cardSort)) if (form is null || form.SchemaVersion is < 1 or > BflStepFormData.CURRENT_SCHEMA_VERSION || IsNull(form.Values) || form.CardSort is { } cardSort && IsBroken(cardSort) || form.Simulation is { } simulation && IsBroken(simulation))
throw new InvalidDataException("This version of AI Studio cannot open the form."); throw new InvalidDataException("This version of AI Studio cannot open the form.");
return form; return form;
} }
/// <summary>
/// Tells whether a stored simulation lacks one of its parts, which JSON can make null, or names a mode this version
/// does not know.
/// </summary>
/// <param name="simulation">The simulation.</param>
private static bool IsBroken(BflSimulation simulation) => !Enum.IsDefined(simulation.Mode) || IsNull(simulation.People) || simulation.People.Any(IsNull);
/// <summary> /// <summary>
/// Tells whether a stored card sort lacks one of its parts, which JSON can make null. /// Tells whether a stored card sort lacks one of its parts, which JSON can make null.
/// </summary> /// </summary>
@@ -0,0 +1,35 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Whom the chat of the step "Test situation" of the BeyondFacts Lab actually plays: the mode and the people which
/// count of what the person chose.
/// </summary>
/// <remarks>
/// A chosen person counts while they are a candidate, see BflSimulationCandidates, and only as many as the mode allows,
/// in the order of the list of participants. When nobody of the chosen people counts, the chat plays generic
/// counterparts, as it did before the person chose a mode.
/// </remarks>
/// <param name="Mode">The mode in effect.</param>
/// <param name="People">The research on the people the chat plays, in the order of the list; empty for generic counterparts.</param>
public sealed record BflEffectiveSimulation(BflSimulationMode Mode, IReadOnlyList<BflPersonResearch> People)
{
/// <summary>
/// The simulation with generic counterparts, no specific person.
/// </summary>
public static readonly BflEffectiveSimulation GENERIC = new(BflSimulationMode.GENERIC, []);
/// <summary>
/// Gets whom the chat plays of what the person chose.
/// </summary>
/// <param name="simulation">What the person chose; null when they chose nothing.</param>
/// <param name="candidates">The people the chat may play, in the order of the list, see BflSimulationCandidates.</param>
/// <returns>The simulation in effect.</returns>
public static BflEffectiveSimulation Of(BflSimulation? simulation, IReadOnlyList<BflPersonResearch> candidates)
{
if (simulation is null || !simulation.Mode.HasPeople)
return GENERIC;
var people = candidates.Where(candidate => simulation.HasChosen(candidate.Name)).Take(simulation.Mode.MaxPeople).ToList();
return people.Count is 0 ? GENERIC : new(simulation.Mode, people);
}
}
@@ -0,0 +1,31 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Whom the person chose for the chat of the step "Test situation" of the BeyondFacts Lab to play: the mode, and the
/// people from the research on single participants.
/// </summary>
/// <remarks>
/// The choice belongs to the details of its step and is stored with them, see BflStepFormData.Simulation. It is the
/// opt-in of the person: the chat plays a real person only while the person chose them here. What counts is the
/// effective simulation, see BflEffectiveSimulation: a name which is no longer a candidate stays stored, but counts no
/// longer, and so do the names beyond what the mode allows.
/// </remarks>
public sealed class BflSimulation
{
/// <summary>
/// Gets or sets the mode.
/// </summary>
public BflSimulationMode Mode { get; set; } = BflSimulationMode.GENERIC;
/// <summary>
/// Gets or sets the names of the people the person chose, as the list of participants gives them, in no particular
/// order.
/// </summary>
public List<string> People { get; set; } = [];
/// <summary>
/// Tells whether the person chose a person, regardless of case and of the spaces between the words.
/// </summary>
/// <param name="name">The name of the person.</param>
public bool HasChosen(string name) => this.People.Any(chosen => string.Equals(BflParticipant.NameKey(chosen), BflParticipant.NameKey(name), StringComparison.Ordinal));
}
@@ -0,0 +1,52 @@
using AIStudio.Models;
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Estimates whether the context window of a model holds the simulation the person chose in the step "Test situation"
/// of the BeyondFacts Lab.
/// </summary>
/// <remarks>
/// The prompt of the step carries the dossier of every person the chat plays, in full: unlike the material, a dossier
/// is never shortened, since the chat must not play a person after half of what they said. The estimate adds the
/// dossiers to the smallest window the lab accepts at all, which leaves room for the fixed part of the prompt, the
/// details, and the conversation, and counts the characters of a dossier the way the budget of the material does. The
/// material then gets what is left, see BflMaterialBudget.
/// </remarks>
public static class BflSimulationBudget
{
/// <summary>
/// Estimates how many tokens the dossiers of the people the chat plays take in the prompt.
/// </summary>
/// <param name="people">The research on the people the chat plays.</param>
/// <returns>The estimated tokens.</returns>
public static int DossierTokens(IReadOnlyList<BflPersonResearch> people)
{
var characters = people.Sum(person => person.Stages.Sum(stage => stage.Research.Text.Trim().Length));
return (characters + BflMaterialBudget.CHARACTERS_PER_TOKEN - 1) / BflMaterialBudget.CHARACTERS_PER_TOKEN;
}
/// <summary>
/// Estimates how many tokens a model needs to read at once for a simulation.
/// </summary>
/// <param name="people">The research on the people the chat plays.</param>
/// <returns>The estimated tokens.</returns>
public static int RequiredTokens(IReadOnlyList<BflPersonResearch> people) => BflModelRequirements.MINIMUM_CONTEXT_TOKENS + DossierTokens(people);
/// <summary>
/// Checks whether the context window of a model is likely to hold a simulation.
/// </summary>
/// <param name="simulation">The simulation in effect.</param>
/// <param name="contextWindow">The context window of the model, as it ships.</param>
/// <returns>Whether the window holds the simulation, nobody knows, or it is likely too small.</returns>
public static BflSimulationFit Check(BflEffectiveSimulation simulation, ContextWindow contextWindow)
{
if (simulation.People.Count is 0)
return BflSimulationFit.FITS;
if (!contextWindow.IsKnown)
return BflSimulationFit.UNKNOWN_CONTEXT;
return contextWindow.DefaultTokens < RequiredTokens(simulation.People) ? BflSimulationFit.TOO_SMALL : BflSimulationFit.FITS;
}
}
@@ -0,0 +1,35 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Decides whom of the participants the chat of the step "Test situation" of the BeyondFacts Lab may play.
/// </summary>
/// <remarks>
/// Only a person with a documented public position qualifies, so that the chat never makes up what somebody stands
/// for: the person stands on the current list of participants, and the stage "Positions" of the research on them is
/// done and holds text. For an event which is not public, no research counts, see BflEventVisibility, and nobody
/// qualifies. The prompt of the simulation still lets the chat refuse to play a person whose dossier documents no
/// position after all, since only the model reads what the text says.
/// </remarks>
public static class BflSimulationCandidates
{
/// <summary>
/// Gets the people the chat of the step "Test situation" may play.
/// </summary>
/// <param name="forms">The forms of the preparation, by their steps.</param>
/// <param name="topics">The research on the topics, with the list of participants.</param>
/// <param name="people">The research on every person stored for the preparation.</param>
/// <returns>The research on each person who qualifies, in the order of the list.</returns>
public static IReadOnlyList<BflPersonResearch> Of(IReadOnlyDictionary<BflStep, BflStepFormData> forms, IReadOnlyDictionary<BflResearchTopic, BflResearch> topics, IReadOnlyList<BflPersonResearch> people)
{
if (BflEventVisibility.IsNotPublic(forms))
return [];
return [.. BflStepResearch.Of(topics, people).People.Where(HasPositions)];
}
/// <summary>
/// Tells whether the research on a person documents public positions: its stage "Positions" is done and holds text.
/// </summary>
/// <param name="person">The research on the person.</param>
public static bool HasPositions(BflPersonResearch person) => person.StageOf(BflPersonResearchStage.POSITIONS) is { } positions && positions.Text.Trim().Length > 0;
}
@@ -0,0 +1,23 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Whether the context window of a model is likely to hold the simulation the person chose in the step "Test
/// situation" of the BeyondFacts Lab, see BflSimulationBudget.
/// </summary>
public enum BflSimulationFit
{
/// <summary>
/// The window is known and holds the simulation, or the simulation plays nobody from the research.
/// </summary>
FITS,
/// <summary>
/// Nobody knows the window of the model, so the lab cannot tell; it warns the person and lets the chat run.
/// </summary>
UNKNOWN_CONTEXT,
/// <summary>
/// The window is known and likely too small; the chat does not send.
/// </summary>
TOO_SMALL,
}
@@ -0,0 +1,31 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// Whom the chat of the step "Test situation" of the BeyondFacts Lab plays: generic counterparts, or people from the
/// research on single participants in a format of the event.
/// </summary>
/// <remarks>
/// The names are stored, so they never change. A new format joins as a new member, see BflSimulationModeExtensions.
/// </remarks>
public enum BflSimulationMode
{
/// <summary>
/// Generic counterparts, no specific person: the step as it was before the lab simulated people.
/// </summary>
GENERIC,
/// <summary>
/// One person from the research, after their dossier.
/// </summary>
SINGLE_PERSON,
/// <summary>
/// A panel discussion with up to three people from the research.
/// </summary>
PANEL_DISCUSSION,
/// <summary>
/// A press conference with up to five people from the research.
/// </summary>
PRESS_CONFERENCE,
}
@@ -0,0 +1,47 @@
namespace AIStudio.Assistants.BeyondFactsLab;
/// <summary>
/// What the BeyondFacts Lab shows and allows for a mode of the simulation in the step "Test situation".
/// </summary>
public static class BflSimulationModeExtensions
{
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BflSimulationModeExtensions).Namespace, nameof(BflSimulationModeExtensions));
/// <param name="mode">The mode.</param>
extension(BflSimulationMode mode)
{
/// <summary>
/// Gets the name of the mode, as the selection shows it.
/// </summary>
public string Title => mode switch
{
BflSimulationMode.SINGLE_PERSON => TB("One person from the research"),
BflSimulationMode.PANEL_DISCUSSION => TB("Panel discussion with up to three people"),
BflSimulationMode.PRESS_CONFERENCE => TB("Press conference with up to five people"),
_ => TB("Generic counterparts, no specific person"),
};
/// <summary>
/// Gets how many people from the research the chat plays at most in this mode; none for generic counterparts.
/// </summary>
public int MaxPeople => mode switch
{
BflSimulationMode.SINGLE_PERSON => 1,
BflSimulationMode.PANEL_DISCUSSION => 3,
BflSimulationMode.PRESS_CONFERENCE => 5,
_ => 0,
};
/// <summary>
/// Gets whether the chat plays people from the research in this mode.
/// </summary>
public bool HasPeople => mode.MaxPeople > 0;
/// <summary>
/// Gets every mode, in the order the selection shows them.
/// </summary>
public static IReadOnlyList<BflSimulationMode> All => Enum.GetValues<BflSimulationMode>();
}
}
@@ -96,7 +96,8 @@ public static class BflStaleness
/// <remarks> /// <remarks>
/// Line endings and spaces around a value do not count, since they change nothing the model reads. The card sort /// Line endings and spaces around a value do not count, since they change nothing the model reads. The card sort
/// counts as a detail of its step; without one, or with an empty one, the fingerprint is the same as before the lab /// counts as a detail of its step; without one, or with an empty one, the fingerprint is the same as before the lab
/// had a card sort, so that no confirmation of an older preparation turns into "Details changed". /// had a card sort, so that no confirmation of an older preparation turns into "Details changed". The same holds for
/// the simulation of the step "Test situation" with generic counterparts.
/// </remarks> /// </remarks>
/// <param name="form">The form.</param> /// <param name="form">The form.</param>
/// <returns>The fingerprint, in hexadecimal digits.</returns> /// <returns>The fingerprint, in hexadecimal digits.</returns>
@@ -115,9 +116,28 @@ public static class BflStaleness
if (form.CardSort is { IsEmpty: false } cardSort) if (form.CardSort is { IsEmpty: false } cardSort)
AppendCardSort(text, cardSort); AppendCardSort(text, cardSort);
if (form.Simulation is { Mode.HasPeople: true } simulation)
AppendSimulation(text, simulation);
return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text.ToString()))); return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text.ToString())));
} }
/// <summary>
/// Appends a simulation with people to the text a fingerprint is computed from.
/// </summary>
/// <remarks>
/// Generic counterparts add nothing, so that the fingerprint stays the same as before the lab simulated people. The
/// people count as a set, by the keys of their names: the order the person chose them in means nothing.
/// </remarks>
/// <param name="text">The text so far.</param>
/// <param name="simulation">The simulation.</param>
private static void AppendSimulation(StringBuilder text, BflSimulation simulation)
{
text.Append(SEPARATOR).Append("simulation").Append(SEPARATOR).Append(simulation.Mode.ToString()).Append(SEPARATOR);
foreach (var key in simulation.People.Select(BflParticipant.NameKey).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal))
text.Append(key).Append(SEPARATOR);
}
/// <summary> /// <summary>
/// Appends a card sort to the text a fingerprint is computed from. /// Appends a card sort to the text a fingerprint is computed from.
/// </summary> /// </summary>
@@ -7,14 +7,15 @@ namespace AIStudio.Assistants.BeyondFactsLab;
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The values are stored under the IDs of the fields of BflFieldCatalog. A value of a field this version does not /// The values are stored under the IDs of the fields of BflFieldCatalog. A value of a field this version does not
/// know, e.g., one a newer version added, stays as it is and is written back unchanged. Version 2 added the card sort. /// know, e.g., one a newer version added, stays as it is and is written back unchanged. Version 2 added the card sort,
/// version 3 the simulation.
/// </remarks> /// </remarks>
public sealed class BflStepFormData public sealed class BflStepFormData
{ {
/// <summary> /// <summary>
/// The version of the stored form this version of AI Studio writes. Raise it with every new property. /// The version of the stored form this version of AI Studio writes. Raise it with every new property.
/// </summary> /// </summary>
public const int CURRENT_SCHEMA_VERSION = 2; public const int CURRENT_SCHEMA_VERSION = 3;
/// <summary> /// <summary>
/// The character which separates the options of a field with several choices. /// The character which separates the options of a field with several choices.
@@ -40,6 +41,15 @@ public sealed class BflStepFormData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public BflCardSort? CardSort { get; set; } public BflCardSort? CardSort { get; set; }
/// <summary>
/// Gets or sets whom the chat of the step "Test situation" plays, see BflSimulation.
/// </summary>
/// <remarks>
/// Null for every other step, and until the person chooses a mode there; the file then leaves it out.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public BflSimulation? Simulation { get; set; }
/// <summary> /// <summary>
/// Gets the value of a field, or an empty string when the field has none. /// Gets the value of a field, or an empty string when the field has none.
/// </summary> /// </summary>
@@ -405,6 +405,54 @@ public sealed class BeyondFactsLabStoreTests
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadFormAsync(created.ProjectId, BflStep.VALUES_AND_PERSPECTIVES)); await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadFormAsync(created.ProjectId, BflStep.VALUES_AND_PERSPECTIVES));
} }
[Test]
public async Task ASimulationIsStoredWithTheFormOfItsStep()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var form = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.PANEL_DISCUSSION, People = ["Eva Klein", "Frank Ott"] } };
await store.SaveFormAsync(created.ProjectId, BflStep.TEST_SITUATION, form);
var json = await File.ReadAllTextAsync(Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps", "06-test", "form.json"));
var loaded = (await this.CreateStore().LoadFormAsync(created.ProjectId, BflStep.TEST_SITUATION)).Simulation;
using (Assert.EnterMultipleScope())
{
Assert.That(json, Does.Contain("\"PANEL_DISCUSSION\""), "The mode is stored by its name, which never changes.");
Assert.That(loaded, Is.Not.Null);
Assert.That(loaded!.Mode, Is.EqualTo(BflSimulationMode.PANEL_DISCUSSION));
Assert.That(loaded.People, Is.EqualTo(new[] { "Eva Klein", "Frank Ott" }));
}
}
[Test]
public async Task AFormOfTheSecondVersionIsStillOpenedWithoutASimulation()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
await this.WriteFormFileAsync(created.ProjectId, "06-test", """{ "schemaVersion": 2, "values": { "test_notes": "Text" } }""");
var form = await store.LoadFormAsync(created.ProjectId, BflStep.TEST_SITUATION);
using (Assert.EnterMultipleScope())
{
Assert.That(form.Get(BflFieldCatalog.TEST_NOTES), Is.EqualTo("Text"));
Assert.That(form.Simulation, Is.Null);
}
}
[TestCase("""{ "schemaVersion": 3, "values": {}, "simulation": { "mode": "PANEL_DISCUSSION", "people": null } }""")]
[TestCase("""{ "schemaVersion": 3, "values": {}, "simulation": { "mode": "PANEL_DISCUSSION", "people": [ null ] } }""")]
[TestCase("""{ "schemaVersion": 3, "values": {}, "simulation": { "mode": 42, "people": [] } }""")]
public async Task AFormWithABrokenSimulationIsNotOpened(string json)
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
await this.WriteFormFileAsync(created.ProjectId, "06-test", json);
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadFormAsync(created.ProjectId, BflStep.TEST_SITUATION));
}
[Test] [Test]
public async Task AStepWithoutAFormGetsAnEmptyOne() public async Task AStepWithoutAFormGetsAnEmptyOne()
{ {
@@ -463,7 +511,7 @@ public sealed class BeyondFactsLabStoreTests
[Test] [Test]
public async Task AFormOfANewerVersionIsNotOverwritten() public async Task AFormOfANewerVersionIsNotOverwritten()
{ {
const string NEWER = """{ "schemaVersion": 3, "values": {} }"""; const string NEWER = """{ "schemaVersion": 4, "values": {} }""";
var store = this.CreateStore(); var store = this.CreateStore();
var created = await store.CreateAsync("Panel"); var created = await store.CreateAsync("Panel");
var formPath = await this.WriteFormFileAsync(created.ProjectId, "05-strategy", NEWER); var formPath = await this.WriteFormFileAsync(created.ProjectId, "05-strategy", NEWER);
@@ -0,0 +1,173 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Models;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks whom the chat of the step "Test situation" of the BeyondFacts Lab may play, whom it plays of what the person
/// chose, and whether a model reads enough for it.
/// </summary>
[TestFixture]
public sealed class BflSimulationTests
{
private static readonly DateTimeOffset CREATED_AT_UTC = new(2026, 11, 15, 10, 0, 0, TimeSpan.Zero);
[Test]
public void OnlyAListedPersonWithDocumentedPositionsQualifies()
{
var forms = Forms();
BflPersonResearch[] people =
[
Person(forms, "Eva Klein", BflPersonResearchStage.All),
Person(forms, "Frank Ott", [BflPersonResearchStage.PROFILE]),
Person(forms, "Gina Roth", [BflPersonResearchStage.PROFILE, BflPersonResearchStage.POSITIONS], positionsText: " "),
Person(forms, "Nicht Gelistet", BflPersonResearchStage.All)
];
var candidates = BflSimulationCandidates.Of(forms, Topics(forms, "Eva Klein", "Frank Ott", "Gina Roth"), people);
Assert.That(candidates.Select(person => person.Name), Is.EqualTo(new[] { "Eva Klein" }));
}
[Test]
public void TheCandidatesComeInTheOrderOfTheList()
{
var forms = Forms();
BflPersonResearch[] people = [Person(forms, "Eva Klein", BflPersonResearchStage.All), Person(forms, "Frank Ott", BflPersonResearchStage.All)];
var candidates = BflSimulationCandidates.Of(forms, Topics(forms, "Frank Ott", "Eva Klein"), people);
Assert.That(candidates.Select(person => person.Name), Is.EqualTo(new[] { "Frank Ott", "Eva Klein" }));
}
[Test]
public void NobodyQualifiesForAnEventWhichIsNotPublic()
{
var forms = Forms();
BflPersonResearch[] people = [Person(forms, "Eva Klein", BflPersonResearchStage.All)];
var topics = Topics(forms, "Eva Klein");
forms[BflStep.FRAMEWORK].Set(BflFieldCatalog.EVENT_VISIBILITY, BflFieldCatalog.EVENT_NOT_PUBLIC);
Assert.That(BflSimulationCandidates.Of(forms, topics, people), Is.Empty);
}
[Test]
public void GenericCounterpartsPlayNobody()
{
var candidates = Candidates("Eva Klein");
using (Assert.EnterMultipleScope())
{
Assert.That(BflEffectiveSimulation.Of(null, candidates), Is.SameAs(BflEffectiveSimulation.GENERIC));
Assert.That(BflEffectiveSimulation.Of(new() { Mode = BflSimulationMode.GENERIC, People = ["Eva Klein"] }, candidates), Is.SameAs(BflEffectiveSimulation.GENERIC));
}
}
[TestCase(BflSimulationMode.SINGLE_PERSON, 1)]
[TestCase(BflSimulationMode.PANEL_DISCUSSION, 3)]
[TestCase(BflSimulationMode.PRESS_CONFERENCE, 5)]
public void AModePlaysAtMostItsNumberOfPeopleInTheOrderOfTheList(BflSimulationMode mode, int expected)
{
string[] names = ["Anna Arendt", "Bruno Berg", "Clara Cramer", "David Dorn", "Eva Klein", "Frank Ott"];
var candidates = Candidates(names);
var simulation = new BflSimulation { Mode = mode, People = [.. names.Reverse()] };
var effective = BflEffectiveSimulation.Of(simulation, candidates);
using (Assert.EnterMultipleScope())
{
Assert.That(mode.MaxPeople, Is.EqualTo(expected));
Assert.That(effective.Mode, Is.EqualTo(mode));
Assert.That(effective.People.Select(person => person.Name), Is.EqualTo(names.Take(expected)));
}
}
[Test]
public void APersonWhoIsNoLongerACandidateCountsNoLonger()
{
var candidates = Candidates("Eva Klein", "Frank Ott");
var simulation = new BflSimulation { Mode = BflSimulationMode.PANEL_DISCUSSION, People = ["Nicht Mehr Gelistet", "frank ott"] };
var effective = BflEffectiveSimulation.Of(simulation, candidates);
using (Assert.EnterMultipleScope())
{
Assert.That(effective.People.Select(person => person.Name), Is.EqualTo(new[] { "Frank Ott" }));
Assert.That(simulation.People, Has.Count.EqualTo(2), "What the person chose stays stored.");
}
}
[Test]
public void AModeWithoutAnyChosenCandidateFallsBackToGenericCounterparts()
{
var candidates = Candidates("Eva Klein");
using (Assert.EnterMultipleScope())
{
Assert.That(BflEffectiveSimulation.Of(new() { Mode = BflSimulationMode.SINGLE_PERSON }, candidates), Is.SameAs(BflEffectiveSimulation.GENERIC));
Assert.That(BflEffectiveSimulation.Of(new() { Mode = BflSimulationMode.PRESS_CONFERENCE, People = ["Frank Ott"] }, candidates), Is.SameAs(BflEffectiveSimulation.GENERIC));
}
}
[Test]
public void TheDossiersAreCountedWithTheCharactersPerTokenOfTheMaterial()
{
var person = new BflPersonResearch { Name = "Eva Klein" };
person.Set(BflPersonResearchStage.PROFILE, new BflResearch { Text = new string('a', 4_000), CreatedAtUtc = CREATED_AT_UTC });
person.Set(BflPersonResearchStage.POSITIONS, new BflResearch { Text = $" {new string('b', 3_999)}\n", CreatedAtUtc = CREATED_AT_UTC });
using (Assert.EnterMultipleScope())
{
Assert.That(BflSimulationBudget.DossierTokens([person]), Is.EqualTo(2_000));
Assert.That(BflSimulationBudget.RequiredTokens([person]), Is.EqualTo(BflModelRequirements.MINIMUM_CONTEXT_TOKENS + 2_000));
}
}
[Test]
public void AKnownWindowWhichIsTooSmallRefusesAndAnUnknownOneWarns()
{
var person = new BflPersonResearch { Name = "Eva Klein" };
person.Set(BflPersonResearchStage.POSITIONS, new BflResearch { Text = new string('a', 40_000), CreatedAtUtc = CREATED_AT_UTC });
var simulation = new BflEffectiveSimulation(BflSimulationMode.SINGLE_PERSON, [person]);
var required = BflSimulationBudget.RequiredTokens([person]);
using (Assert.EnterMultipleScope())
{
Assert.That(BflSimulationBudget.Check(simulation, ContextWindow.Of(required - 1)), Is.EqualTo(BflSimulationFit.TOO_SMALL));
Assert.That(BflSimulationBudget.Check(simulation, ContextWindow.Of(required)), Is.EqualTo(BflSimulationFit.FITS));
Assert.That(BflSimulationBudget.Check(simulation, ContextWindow.UNKNOWN), Is.EqualTo(BflSimulationFit.UNKNOWN_CONTEXT));
Assert.That(BflSimulationBudget.Check(BflEffectiveSimulation.GENERIC, ContextWindow.Of(8_192)), Is.EqualTo(BflSimulationFit.FITS), "Generic counterparts need no room for dossiers.");
Assert.That(BflSimulationBudget.Check(BflEffectiveSimulation.GENERIC, ContextWindow.UNKNOWN), Is.EqualTo(BflSimulationFit.FITS));
}
}
private static List<BflPersonResearch> Candidates(params string[] names)
{
var forms = Forms();
return [.. BflSimulationCandidates.Of(forms, Topics(forms, names), [.. names.Select(name => Person(forms, name, BflPersonResearchStage.All))])];
}
private static BflPersonResearch Person(IReadOnlyDictionary<BflStep, BflStepFormData> forms, string name, IEnumerable<BflPersonResearchStage> stages, string? positionsText = null)
{
var person = new BflPersonResearch { Name = name };
foreach (var stage in stages)
person.Set(stage, BflResearch.Of(stage is BflPersonResearchStage.POSITIONS && positionsText is not null ? positionsText : $"Text {stage}", [], [], BflPromptComposer.PublicDetailsOf(forms), CREATED_AT_UTC));
return person;
}
private static Dictionary<BflResearchTopic, BflResearch> Topics(IReadOnlyDictionary<BflStep, BflStepFormData> forms, params string[] names)
{
var list = BflResearch.Of(string.Join("\n\n", names.Select(name => $"### {name}")), [], [], BflPromptComposer.PublicDetailsOf(forms), CREATED_AT_UTC);
list.Participants = [.. BflParticipant.ListIn(list.Text)];
return new() { [BflResearchTopic.PARTICIPANTS] = list };
}
private static Dictionary<BflStep, BflStepFormData> Forms()
{
var forms = BflStep.All.ToDictionary(step => step, _ => new BflStepFormData());
forms[BflStep.FRAMEWORK].Set(BflFieldCatalog.FORMAT, "Podium");
forms[BflStep.FRAMEWORK].Set(BflFieldCatalog.EVENT_TOPIC, "Klimapolitik");
return forms;
}
}
@@ -213,6 +213,46 @@ public sealed class BflStalenessTests
Assert.That(BflStaleness.FingerprintOf(first), Is.EqualTo(BflStaleness.FingerprintOf(second))); Assert.That(BflStaleness.FingerprintOf(first), Is.EqualTo(BflStaleness.FingerprintOf(second)));
} }
[Test]
public void WithGenericCounterpartsTheFingerprintStaysAsItWasBeforeTheSimulation()
{
var withoutSimulation = new BflStepFormData();
withoutSimulation.Set(BflFieldCatalog.TEST_NOTES, "Text");
var generic = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.GENERIC, People = ["Eva Klein"] } };
generic.Set(BflFieldCatalog.TEST_NOTES, "Text");
// How the lab computed the fingerprint of this form before it simulated people:
var before = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes($"{BflFieldCatalog.TEST_NOTES}\0Text\0")));
using (Assert.EnterMultipleScope())
{
Assert.That(BflStaleness.FingerprintOf(withoutSimulation), Is.EqualTo(before));
Assert.That(BflStaleness.FingerprintOf(generic), Is.EqualTo(before));
}
}
[Test]
public void TheFingerprintCountsTheModeAndThePeopleOfTheSimulation()
{
var empty = new BflStepFormData();
var single = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.SINGLE_PERSON, People = ["Eva Klein"] } };
var panel = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.PANEL_DISCUSSION, People = ["Eva Klein"] } };
var otherPerson = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.SINGLE_PERSON, People = ["Frank Ott"] } };
var fingerprints = new[] { empty, single, panel, otherPerson }.Select(BflStaleness.FingerprintOf).ToList();
Assert.That(fingerprints, Is.Unique);
}
[Test]
public void TheOrderAndTheSpellingOfTheSimulatedPeopleIsNoChange()
{
var first = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.PANEL_DISCUSSION, People = ["Eva Klein", "Frank Ott"] } };
var second = new BflStepFormData { Simulation = new() { Mode = BflSimulationMode.PANEL_DISCUSSION, People = ["frank ott", "Eva Klein"] } };
Assert.That(BflStaleness.FingerprintOf(first), Is.EqualTo(BflStaleness.FingerprintOf(second)));
}
[Test] [Test]
public void AChangedCardSortWeighsLikeChangedDetails() public void AChangedCardSortWeighsLikeChangedDetails()
{ {