mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-15 21:28:27 +00:00
Added the choice of simulated participants to the test situation of the BeyondFacts Lab
This commit is contained in:
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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user