Added the guided chat to the BeyondFacts Lab (#1177)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-09 09:55:01 +01:00
1 parent e77645f44c
commit 79e0c80031
55 files changed
+3481 -72

No files matched your search

@@ -436,6 +436,34 @@ public sealed class BeyondFactsLabStoreTests
}
}
[Test]
public async Task TheProviderIsRememberedWithoutCountingAsAChange()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
await store.SetProviderAsync(created.ProjectId, "provider-1");
var loaded = await this.CreateStore().LoadAsync(created.ProjectId);
using (Assert.EnterMultipleScope())
{
Assert.That(created.ProviderId, Is.Empty, "A new preparation starts with the preselected provider.");
Assert.That(loaded!.ProviderId, Is.EqualTo("provider-1"));
Assert.That(loaded.ModifiedAtUtc, Is.EqualTo(created.ModifiedAtUtc));
}
}
[Test]
public async Task APreparationWithoutAProviderIdIsNotOpened()
{
var projectId = Guid.NewGuid();
await this.WriteProjectFileAsync(projectId, $$"""{ "schemaVersion": 1, "projectId": "{{projectId:D}}", "name": "Panel", "providerId": null }""");
var loaded = await this.CreateStore().LoadAsync(projectId);
Assert.That(loaded, Is.Null);
}
[Test]
public async Task APreparationWithAnUnknownStepIsNotOpened()
{
@@ -0,0 +1,178 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Chat;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks how the BeyondFacts Lab stores the chats of the steps.
/// </summary>
[TestFixture]
public sealed class BeyondFactsLabStoreThreadTests
{
private string dataDirectory = string.Empty;
private string RootDirectory => Path.Combine(this.dataDirectory, BeyondFactsLabStore.ROOT_DIRECTORY_NAME);
[SetUp]
public void CreateDataDirectory()
{
this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-beyondfacts-lab-threads-{Guid.NewGuid():N}");
Directory.CreateDirectory(this.dataDirectory);
}
[TearDown]
public void DeleteDataDirectory()
{
if (Directory.Exists(this.dataDirectory))
Directory.Delete(this.dataDirectory, true);
}
[Test]
public async Task AStepWithoutAChatHasNone()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var thread = await store.LoadThreadAsync(new BflThreadLocation(created.ProjectId, BflStep.FRAMEWORK));
Assert.That(thread, Is.Null);
}
[Test]
public async Task AStoredChatIsLoadedWithItsMessages()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var location = new BflThreadLocation(created.ProjectId, BflStep.KNOWLEDGE);
var thread = WithMessage(BflThreadFactory.Create(location), "Worauf beruht meine Expertise?");
var changed = await store.SaveThreadAsync(location, thread);
var loaded = await this.CreateStore().LoadThreadAsync(location);
using (Assert.EnterMultipleScope())
{
Assert.That(loaded, Is.Not.Null);
Assert.That(loaded!.ChatId, Is.EqualTo(location.ChatId));
Assert.That(loaded.Blocks, Has.Count.EqualTo(1));
Assert.That(((ContentText)loaded.Blocks[0].Content!).Text, Is.EqualTo("Worauf beruht meine Expertise?"));
Assert.That(changed.ModifiedAtUtc, Is.GreaterThan(created.ModifiedAtUtc), "A conversation is work on the preparation.");
Assert.That(File.Exists(Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps", "02-knowledge", "thread.json")), Is.True);
}
}
[Test]
public async Task AChatOfAnotherStepIsNotStored()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var thread = BflThreadFactory.Create(new BflThreadLocation(created.ProjectId, BflStep.STRATEGY));
await Assert.ThrowsAsync<ArgumentException>(async () => await store.SaveThreadAsync(new BflThreadLocation(created.ProjectId, BflStep.FRAMEWORK), thread));
}
[Test]
public async Task AChatOfADeletedPreparationIsNotStored()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var location = new BflThreadLocation(created.ProjectId, BflStep.FRAMEWORK);
await store.DeleteAsync(created.ProjectId);
await Assert.ThrowsAsync<FileNotFoundException>(async () => await store.SaveThreadAsync(location, BflThreadFactory.Create(location)));
Assert.That(Directory.Exists(Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"))), Is.False, "An answer which ends after the deletion must not bring the preparation back.");
}
[Test]
public async Task AChatCopiedIntoAnotherStepIsNotOpened()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var strategy = new BflThreadLocation(created.ProjectId, BflStep.STRATEGY);
var framework = new BflThreadLocation(created.ProjectId, BflStep.FRAMEWORK);
await store.SaveThreadAsync(strategy, BflThreadFactory.Create(strategy));
var stepsDirectory = Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps");
Directory.CreateDirectory(Path.Combine(stepsDirectory, "01-framework"));
File.Copy(Path.Combine(stepsDirectory, "05-strategy", "thread.json"), Path.Combine(stepsDirectory, "01-framework", "thread.json"));
using (Assert.EnterMultipleScope())
{
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadThreadAsync(framework));
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.SaveThreadAsync(framework, BflThreadFactory.Create(framework)));
}
}
[Test]
public async Task ADamagedChatIsNeitherOpenedNorOverwritten()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var location = new BflThreadLocation(created.ProjectId, BflStep.FRAMEWORK);
var path = Path.Combine(this.RootDirectory, created.ProjectId.ToString("D"), "steps", "01-framework", "thread.json");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, "{ damaged");
using (Assert.EnterMultipleScope())
{
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.LoadThreadAsync(location));
await Assert.ThrowsAsync<InvalidDataException>(async () => await store.SaveThreadAsync(location, BflThreadFactory.Create(location)));
Assert.That(await File.ReadAllTextAsync(path), Is.EqualTo("{ damaged"));
}
}
[Test]
public async Task StartingOverDeletesTheChat()
{
var store = this.CreateStore();
var created = await store.CreateAsync("Panel");
var location = new BflThreadLocation(created.ProjectId, BflStep.FRAMEWORK);
await store.SaveThreadAsync(location, WithMessage(BflThreadFactory.Create(location), "Hallo"));
await store.DeleteThreadAsync(location);
Assert.That(await store.LoadThreadAsync(location), Is.Null);
}
[Test]
public async Task EveryChatOnTheDiskIsFoundByItsId()
{
var store = this.CreateStore();
var first = await store.CreateAsync("Panel");
var second = await store.CreateAsync("Interview");
var firstStrategy = new BflThreadLocation(first.ProjectId, BflStep.STRATEGY);
var secondFramework = new BflThreadLocation(second.ProjectId, BflStep.FRAMEWORK);
await store.SaveThreadAsync(firstStrategy, BflThreadFactory.Create(firstStrategy));
await store.SaveThreadAsync(secondFramework, BflThreadFactory.Create(secondFramework));
// A preparation this version cannot open keeps its chats all the same:
await File.WriteAllTextAsync(Path.Combine(this.RootDirectory, second.ProjectId.ToString("D"), "project.json"), "{ damaged");
var locations = this.CreateStore().ReadAllThreadLocations();
using (Assert.EnterMultipleScope())
{
Assert.That(locations, Has.Count.EqualTo(2));
Assert.That(locations[firstStrategy.ChatId], Is.EqualTo(firstStrategy));
Assert.That(locations[secondFramework.ChatId], Is.EqualTo(secondFramework));
Assert.That(store.GetThreadWrittenAt(firstStrategy), Is.Not.Null);
Assert.That(store.GetThreadWrittenAt(new BflThreadLocation(first.ProjectId, BflStep.FRAMEWORK)), Is.Null);
}
}
private static ChatThread WithMessage(ChatThread thread, string text)
{
thread.Blocks.Add(new ContentBlock
{
Time = DateTimeOffset.Now,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText { Text = text }
});
return thread;
}
private BeyondFactsLabStore CreateStore() => new(NullLogger<BeyondFactsLabStore>.Instance, new BflStorageOptions { DataDirectory = this.dataDirectory });
}
@@ -0,0 +1,32 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Models;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks which models the BeyondFacts Lab accepts for its chats.
/// </summary>
[TestFixture]
public sealed class BflModelRequirementsTests
{
[Test]
public void AModelWithoutAStatementAboutItsWindowIsAccepted()
{
Assert.That(BflModelRequirements.HasEnoughContext(ContextWindow.UNKNOWN), Is.True);
}
[TestCase(8_192, false)]
[TestCase(BflModelRequirements.MINIMUM_CONTEXT_TOKENS - 1, false)]
[TestCase(BflModelRequirements.MINIMUM_CONTEXT_TOKENS, true)]
[TestCase(128_000, true)]
public void AKnownWindowNeedsTheMinimum(int defaultTokens, bool expected)
{
Assert.That(BflModelRequirements.HasEnoughContext(ContextWindow.Of(defaultTokens)), Is.EqualTo(expected));
}
[Test]
public void AWindowWhichOnlyAnOperatorCouldRaiseIsNotEnough()
{
Assert.That(BflModelRequirements.HasEnoughContext(ContextWindow.Of(16_384, 131_072)), Is.False, "What a model reads as it ships counts, not what it might read.");
}
}
@@ -0,0 +1,172 @@
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{{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 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.");
}
}
}
@@ -0,0 +1,69 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Settings.DataModel;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks that AI Studio comes with every prompt of the BeyondFacts Lab, and that they stay small enough.
/// </summary>
/// <remarks>
/// The tests run a development build, which reads the prompts from its output folder: the project file copies every
/// embedded file there as well.
/// </remarks>
[TestFixture]
public sealed class BflPromptLibraryTests
{
/// <summary>
/// How many characters the fixed part of the longest system prompt may have: the core, the guide, the longest step,
/// and the form of address.
/// </summary>
/// <remarks>
/// German takes about four characters per token, so this is about 22,000 tokens. The lab accepts models with a
/// context of 32,000 tokens; the rest is left for the details of the person and the conversation. Raising this
/// limit means raising that minimum, too.
/// </remarks>
private const int MAX_FIXED_PROMPT_CHARACTERS = 90_000;
[Test]
public async Task EveryPromptComesWithTheApp()
{
foreach (var path in BflPromptLibrary.AllPaths)
Assert.That(await BflPromptLibrary.ReadAsync(path), Is.Not.Empty, path);
}
[Test]
public void EveryStepHasAPromptOfItsOwn()
{
var paths = BflStep.All.Select(BflPromptLibrary.StepPath).ToList();
Assert.That(paths, Is.Unique);
}
[Test]
public void BothFormsOfAddressHaveAnInstructionOfTheirOwn()
{
Assert.That(BflPromptLibrary.AddressPath(AddressForm.INFORMAL), Is.Not.EqualTo(BflPromptLibrary.AddressPath(AddressForm.FORMAL)));
}
[Test]
public async Task AMissingPromptIsAnError()
{
await Assert.ThrowsAsync<FileNotFoundException>(async () => await BflPromptLibrary.ReadAsync("Assistants/BeyondFactsLab/Prompts/does-not-exist.md"));
}
[Test]
public async Task TheFixedPartOfTheLongestPromptLeavesRoomForTheConversation()
{
var core = await BflPromptLibrary.ReadAsync(BflPromptLibrary.CORE_PATH);
var knowledge = await BflPromptLibrary.ReadAsync(BflPromptLibrary.KNOWLEDGE_PATH);
var longestStep = 0;
foreach (var step in BflStep.All)
longestStep = Math.Max(longestStep, (await BflPromptLibrary.ReadAsync(BflPromptLibrary.StepPath(step))).Length);
var longestAddress = 0;
foreach (var addressForm in Enum.GetValues<AddressForm>())
longestAddress = Math.Max(longestAddress, (await BflPromptLibrary.ReadAsync(BflPromptLibrary.AddressPath(addressForm))).Length);
Assert.That(core.Length + knowledge.Length + longestStep + longestAddress, Is.LessThanOrEqualTo(MAX_FIXED_PROMPT_CHARACTERS));
}
}
@@ -0,0 +1,94 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks the IDs of the chats of the BeyondFacts Lab and how the lab sets them up.
/// </summary>
[TestFixture]
public sealed class BflThreadTests
{
private static readonly Guid PROJECT_ID = Guid.Parse("6f1c2b0e-3a51-4c3e-9d2f-0b7a4e5c8d91");
[Test]
public void TheIdOfAChatFollowsFromItsPlace()
{
// Computed independently, from SHA-256 over "beyondfacts-lab/thread/<project>/05-strategy". A stored chat keeps
// its ID, so this value never changes:
var expected = Guid.Parse("a6e33c75-66c3-8ac8-bae2-8b6a90fb6f19");
using (Assert.EnterMultipleScope())
{
Assert.That(BflThreadId.Of(PROJECT_ID, BflStep.STRATEGY), Is.EqualTo(expected));
Assert.That(new BflThreadLocation(PROJECT_ID, BflStep.STRATEGY).ChatId, Is.EqualTo(expected));
}
}
[Test]
public void EveryChatHasAnIdOfItsOwn()
{
var otherProject = Guid.NewGuid();
var ids = BflStep.All.SelectMany(step => new[] { BflThreadId.Of(PROJECT_ID, step), BflThreadId.Of(otherProject, step) }).ToList();
using (Assert.EnterMultipleScope())
{
Assert.That(ids, Is.Unique);
Assert.That(ids, Has.None.EqualTo(Guid.Empty));
Assert.That(ids, Has.None.EqualTo(PROJECT_ID));
}
}
[Test]
public void TheIdIsAVersion8Uuid()
{
var id = BflThreadId.Of(PROJECT_ID, BflStep.FRAMEWORK);
using (Assert.EnterMultipleScope())
{
Assert.That(id.Version, Is.EqualTo(8));
Assert.That(id.Variant & 0xC, Is.EqualTo(0x8), "The variant of RFC 9562.");
}
}
[Test]
public void ANewChatHasNothingButItsPlace()
{
var location = new BflThreadLocation(PROJECT_ID, BflStep.KNOWLEDGE);
var thread = BflThreadFactory.Create(location);
using (Assert.EnterMultipleScope())
{
Assert.That(thread.ChatId, Is.EqualTo(location.ChatId));
Assert.That(thread.Blocks, Is.Empty);
Assert.That(thread.SelectedProfile, Is.EqualTo(Profile.NO_PROFILE.Id));
Assert.That(thread.SelectedChatTemplate, Is.EqualTo(ChatTemplate.NO_CHAT_TEMPLATE.Id));
Assert.That(thread.SelectedToolIds, Is.Empty);
Assert.That(thread.DataSourceOptions.DisableDataSources, Is.True);
}
}
[Test]
public void AChatIsReadiedAsAChatOfTheLabWithoutToolsOrDataSources()
{
var thread = BflThreadFactory.Create(new BflThreadLocation(PROJECT_ID, BflStep.KNOWLEDGE));
thread.DataSourceOptions.DisableDataSources = false;
thread.RuntimeSelectedToolIds = ["web_search"];
var provider = new AIStudio.Settings.Provider(1, "provider-1", "Test", LLMProviders.SELF_HOSTED, new Model("model", null));
BflThreadFactory.PrepareForRequest(thread, provider, "Prompt");
using (Assert.EnterMultipleScope())
{
Assert.That(thread.SystemPrompt, Is.EqualTo("Prompt"));
Assert.That(thread.SelectedProvider, Is.EqualTo("provider-1"));
Assert.That(thread.RuntimeComponent, Is.EqualTo(AIStudio.Tools.Components.BEYOND_FACTS_LAB_ASSISTANT));
Assert.That(thread.RuntimeToolsAreAssistantManaged, Is.True);
Assert.That(thread.RuntimeSelectedToolIds, Is.Empty);
Assert.That(thread.DataSourceOptions.DisableDataSources, Is.True);
Assert.That(thread.SelectedProfile, Is.EqualTo(Profile.NO_PROFILE.Id));
}
}
}
@@ -0,0 +1,49 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Chat;
using AIStudio.Models;
namespace AIStudio.Tests.BeyondFactsLab;
/// <summary>
/// Checks when the BeyondFacts Lab warns about a full context, and when it stops sending.
/// </summary>
[TestFixture]
public sealed class BflTokenBudgetTests
{
private const int WINDOW = 32_000;
[Test]
public void ACountWhichFailedTellsNothing()
{
Assert.That(BflTokenBudget.Of(ConversationTokens.UNAVAILABLE), Is.EqualTo(BflTokenBudgetState.UNKNOWN));
}
[Test]
public void AModelWithoutAStatementAboutItsWindowTellsNothing()
{
var tokens = new ConversationTokens { IsKnown = true, HistoryTokens = 500_000, Window = ContextWindow.UNKNOWN };
Assert.That(BflTokenBudget.Of(tokens), Is.EqualTo(BflTokenBudgetState.UNKNOWN), "The lab cannot know when such a model is full, so it never stops sending.");
}
[TestCase(0, BflTokenBudgetState.OK)]
[TestCase(25_599, BflTokenBudgetState.OK)]
[TestCase(25_600, BflTokenBudgetState.WARNING)]
[TestCase(WINDOW - BflTokenBudget.RESERVE_TOKENS, BflTokenBudgetState.WARNING)]
[TestCase(WINDOW - BflTokenBudget.RESERVE_TOKENS + 1, BflTokenBudgetState.FULL)]
[TestCase(WINDOW * 2, BflTokenBudgetState.FULL)]
public void TheShareOfTheWindowDecides(int historyTokens, BflTokenBudgetState expected)
{
var tokens = new ConversationTokens { IsKnown = true, HistoryTokens = historyTokens, Window = ContextWindow.Of(WINDOW) };
Assert.That(BflTokenBudget.Of(tokens), Is.EqualTo(expected));
}
[Test]
public void TheMessageBeingWrittenCountsAsWell()
{
var tokens = new ConversationTokens { IsKnown = true, HistoryTokens = 20_000, DraftTokens = 6_000, Window = ContextWindow.Of(WINDOW) };
Assert.That(BflTokenBudget.Of(tokens), Is.EqualTo(BflTokenBudgetState.WARNING));
}
}