mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-15 11:48:27 +00:00
Moved the chats of the BeyondFacts Lab to the embedded chat of assistants (#1203)
This commit is contained in:
1 parent
dc97db256e
commit
cb54ad297d
27 files changed
+1023
-743
No files matched your search
@@ -4,13 +4,15 @@ using AIStudio.Chat.Embedded;
|
||||
namespace AIStudio.Tests.Chat.Embedded;
|
||||
|
||||
/// <summary>
|
||||
/// Checks when an embedded chat sends the message it opens with.
|
||||
/// Checks when an embedded chat sends the message it opens with, and when it offers to let the model begin.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class EmbeddedChatOpeningTests
|
||||
{
|
||||
private static readonly ContentText OPENING_MESSAGE = new() { Text = "What is new today?" };
|
||||
|
||||
private const string START_REQUEST = "Open the conversation with your first message.";
|
||||
|
||||
[Test]
|
||||
public void AnEmptyChatSendsItsOpeningMessage()
|
||||
{
|
||||
@@ -61,6 +63,57 @@ public sealed class EmbeddedChatOpeningTests
|
||||
Assert.That(EmbeddedChatOpening.SendsNow(OPENING_MESSAGE, conversation, wasSent: false), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnEmptyChatOffersToBegin()
|
||||
{
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(START_REQUEST, Conversation()), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AChatWithoutStartRequestOffersNothing()
|
||||
{
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(null, Conversation()), Is.False);
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(" ", Conversation()), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AChatWhoseConversationBeganOffersNothing()
|
||||
{
|
||||
var conversation = Conversation();
|
||||
conversation.Thread!.Blocks.Add(new ContentBlock { Role = ChatRole.AI, ContentType = ContentType.TEXT, Content = new ContentText { Text = "Hello, what do you prepare?" } });
|
||||
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(START_REQUEST, conversation), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AChatWithOnlyAHiddenRequestStillOffersToBegin()
|
||||
{
|
||||
// E.g., the model failed to answer the request which lets it begin:
|
||||
var conversation = Conversation();
|
||||
conversation.Thread!.Blocks.Add(new ContentBlock { Role = ChatRole.USER, ContentType = ContentType.TEXT, Content = new ContentText { Text = START_REQUEST }, HideFromUser = true });
|
||||
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(START_REQUEST, conversation), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AChatWhichLoadsRunsOrCannotBeOpenedOffersNothing()
|
||||
{
|
||||
var running = Conversation();
|
||||
running.IsRunning = true;
|
||||
var unavailable = Conversation();
|
||||
unavailable.Thread = null;
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(START_REQUEST, null), Is.False);
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(START_REQUEST, running), Is.False);
|
||||
Assert.That(EmbeddedChatOpening.OffersStart(START_REQUEST, unavailable), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
private static EmbeddedConversation Conversation() => new()
|
||||
{
|
||||
ChatId = Guid.NewGuid(),
|
||||
|
||||
@@ -130,7 +130,7 @@ public sealed class EmbeddedChatServiceTests
|
||||
var service = this.CreateService(Answer("An answer."));
|
||||
|
||||
var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread());
|
||||
var sent = await service.SendAsync(chatId, this.Request("A question.") with { PrepareAsync = _ => Task.FromResult(false) });
|
||||
var sent = await service.SendAsync(chatId, this.Request("A question.") with { PrepareAsync = _ => Task.FromResult(EmbeddedChatPreparation.REFUSED) });
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
@@ -202,7 +202,8 @@ public sealed class EmbeddedChatServiceTests
|
||||
|
||||
await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread());
|
||||
var extraBlock = new ContentBlock { Role = ChatRole.USER, ContentType = ContentType.TEXT, Content = new ContentText { Text = "An image description." } };
|
||||
await service.SendAsync(chatId, this.Request("A question.") with { ThreadForRequest = thread => thread with { Blocks = [extraBlock, .. thread.Blocks] } });
|
||||
var preparation = EmbeddedChatPreparation.READY with { ThreadForRequest = thread => thread with { Blocks = [extraBlock, .. thread.Blocks] } };
|
||||
await service.SendAsync(chatId, this.Request("A question.") with { PrepareAsync = _ => Task.FromResult(preparation) });
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
@@ -228,6 +229,83 @@ public sealed class EmbeddedChatServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StartingOverDeletesTheConversationAndKeepsTheIdOfTheChat()
|
||||
{
|
||||
var store = new InMemoryEmbeddedChatStore();
|
||||
var chatId = Guid.NewGuid();
|
||||
var service = this.CreateService(Answer("An answer."));
|
||||
|
||||
var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread());
|
||||
await service.SendAsync(chatId, this.Request("A question."));
|
||||
var reset = await service.ResetAsync(chatId, () => new ChatThread { Name = "Started over" });
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reset, Is.True);
|
||||
Assert.That(store.IsStored(chatId), Is.False, "The stored conversation is gone.");
|
||||
Assert.That(conversation.Thread!.Blocks, Is.Empty);
|
||||
Assert.That(conversation.Thread.ChatId, Is.EqualTo(chatId));
|
||||
Assert.That(conversation.Thread.Name, Is.EqualTo("Started over"), "The new conversation starts as a new chat does.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AConversationDoesNotStartOverWhileTheModelAnswers()
|
||||
{
|
||||
var store = new InMemoryEmbeddedChatStore();
|
||||
var chatId = Guid.NewGuid();
|
||||
var answering = new TaskCompletionSource();
|
||||
var release = new TaskCompletionSource();
|
||||
var service = this.CreateService(async (_, answer, _, _) =>
|
||||
{
|
||||
answering.SetResult();
|
||||
await release.Task;
|
||||
answer.Text = "An answer.";
|
||||
});
|
||||
|
||||
var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread());
|
||||
var sending = service.SendAsync(chatId, this.Request("A question."));
|
||||
await answering.Task;
|
||||
|
||||
var reset = await service.ResetAsync(chatId, () => new ChatThread());
|
||||
release.SetResult();
|
||||
await sending;
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reset, Is.False);
|
||||
Assert.That(conversation.Thread!.Blocks, Has.Count.EqualTo(2), "The question and its answer stay.");
|
||||
Assert.That(store.IsStored(chatId), Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AChatThisVersionCannotOpenBecomesAvailableWhenItStartsOver()
|
||||
{
|
||||
var store = new InMemoryEmbeddedChatStore { CannotOpen = true };
|
||||
var chatId = Guid.NewGuid();
|
||||
var service = this.CreateService(Answer("An answer."));
|
||||
|
||||
var conversation = await service.GetAsync(store, COMPONENT, chatId, () => new ChatThread());
|
||||
var wasAvailable = conversation.IsAvailable;
|
||||
var reset = await service.ResetAsync(chatId, () => new ChatThread());
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(wasAvailable, Is.False);
|
||||
Assert.That(reset, Is.True);
|
||||
Assert.That(conversation.IsAvailable, Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AChatWhichWasNeverLoadedDoesNotStartOver()
|
||||
{
|
||||
var service = this.CreateService(Answer("An answer."));
|
||||
Assert.That(await service.ResetAsync(Guid.NewGuid(), () => new ChatThread()), Is.False);
|
||||
}
|
||||
|
||||
private EmbeddedChatService CreateService(Func<EmbeddedChatRequest, ContentText, ChatThread, CancellationToken, Task> answerAsync) => new(this.settingsManager, new AssistantSessionService(MessageBus.INSTANCE), MessageBus.INSTANCE, NullLogger<EmbeddedChatService>.Instance, answerAsync);
|
||||
|
||||
private EmbeddedChatRequest Request(string text) => new() { Provider = this.provider, Message = new ContentText { Text = text } };
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Chat.Embedded;
|
||||
using AIStudio.Models;
|
||||
|
||||
namespace AIStudio.Tests.Chat.Embedded;
|
||||
|
||||
/// <summary>
|
||||
/// Checks when an embedded chat which shows the context window warns about a full context, and when it stops sending.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class EmbeddedChatTokenBudgetTests
|
||||
{
|
||||
private const int WINDOW = 32_000;
|
||||
|
||||
[Test]
|
||||
public void ACountWhichFailedTellsNothing()
|
||||
{
|
||||
Assert.That(EmbeddedChatTokenBudget.Of(ConversationTokens.UNAVAILABLE), Is.EqualTo(EmbeddedChatTokenBudgetState.UNKNOWN));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AModelWithoutAStatementAboutItsWindowTellsNothing()
|
||||
{
|
||||
var tokens = new ConversationTokens { IsKnown = true, HistoryTokens = 500_000, Window = ContextWindow.UNKNOWN };
|
||||
|
||||
Assert.That(EmbeddedChatTokenBudget.Of(tokens), Is.EqualTo(EmbeddedChatTokenBudgetState.UNKNOWN), "The chat cannot know when such a model is full, so it never stops sending.");
|
||||
}
|
||||
|
||||
[TestCase(0, EmbeddedChatTokenBudgetState.OK)]
|
||||
[TestCase(25_599, EmbeddedChatTokenBudgetState.OK)]
|
||||
[TestCase(25_600, EmbeddedChatTokenBudgetState.WARNING)]
|
||||
[TestCase(WINDOW - EmbeddedChatTokenBudget.RESERVE_TOKENS, EmbeddedChatTokenBudgetState.WARNING)]
|
||||
[TestCase(WINDOW - EmbeddedChatTokenBudget.RESERVE_TOKENS + 1, EmbeddedChatTokenBudgetState.FULL)]
|
||||
[TestCase(WINDOW * 2, EmbeddedChatTokenBudgetState.FULL)]
|
||||
public void TheShareOfTheWindowDecides(int historyTokens, EmbeddedChatTokenBudgetState expected)
|
||||
{
|
||||
var tokens = new ConversationTokens { IsKnown = true, HistoryTokens = historyTokens, Window = ContextWindow.Of(WINDOW) };
|
||||
|
||||
Assert.That(EmbeddedChatTokenBudget.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(EmbeddedChatTokenBudget.Of(tokens), Is.EqualTo(EmbeddedChatTokenBudgetState.WARNING));
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,12 @@ public sealed class InMemoryEmbeddedChatStore : IEmbeddedChatStore
|
||||
/// <param name="chatId">The ID of the chat.</param>
|
||||
public IReadOnlyList<ContentBlock> StoredBlocks(Guid chatId) => this.threads.TryGetValue(chatId, out var thread) ? thread.Blocks : [];
|
||||
|
||||
/// <summary>
|
||||
/// Tells whether a chat is stored.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The ID of the chat.</param>
|
||||
public bool IsStored(Guid chatId) => this.threads.ContainsKey(chatId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ChatThread?> LoadThreadAsync(Guid chatId, CancellationToken token = default)
|
||||
{
|
||||
@@ -43,4 +49,11 @@ public sealed class InMemoryEmbeddedChatStore : IEmbeddedChatStore
|
||||
this.SaveCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteThreadAsync(Guid chatId, CancellationToken token = default)
|
||||
{
|
||||
this.threads.Remove(chatId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user