mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-11 02:47:22 +00:00
Added an introduction and an onboarding chat to the BeyondFacts Lab (#1178)
This commit is contained in:
1 parent
79e0c80031
commit
22d94eb8fd
35 files changed
+1377
-361
No files matched your search
@@ -8,7 +8,7 @@ using AIStudio.Tools.AssistantSessions;
|
||||
namespace AIStudio.Assistants.BeyondFactsLab;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the chats of the steps of the BeyondFacts Lab.
|
||||
/// Runs the chats of the BeyondFacts Lab, such as those of the steps of a preparation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A singleton, so that an answer goes on while the person looks at another page: the page asks for the chat again
|
||||
@@ -30,17 +30,21 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
/// </remarks>
|
||||
public const string OPENING_REQUEST = "Beginne das Gespräch dieses Schritts mit deiner ersten Nachricht, wie es der Abschnitt „Dieser Schritt“ beschreibt.";
|
||||
|
||||
/// <summary>
|
||||
/// The request which lets the model open the conversation about whether the lab fits a task. The person does not
|
||||
/// see it.
|
||||
/// </summary>
|
||||
public const string ONBOARDING_OPENING_REQUEST = "Beginne das Gespräch mit deiner ersten Nachricht, wie es der Abschnitt „Gesprächsführung“ beschreibt.";
|
||||
|
||||
/// <summary>
|
||||
/// How often a streamed answer renders at most.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan STREAMING_RENDER_MIN_TIME = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BflConversationService).Namespace, nameof(BflConversationService));
|
||||
|
||||
private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(BflConversationService).Namespace, nameof(BflConversationService));
|
||||
|
||||
/// <summary>
|
||||
/// The chats loaded so far, by their steps.
|
||||
/// The chats loaded so far, by their places.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<BflThreadLocation, BflConversation> conversations = new();
|
||||
|
||||
@@ -63,9 +67,9 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
public event Func<BflThreadLocation, Task>? ConversationChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat of a step, loading it on the first call.
|
||||
/// Gets a chat, loading it on the first call.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>The chat; it is not available when this version cannot open the stored one.</returns>
|
||||
public async Task<BflConversation> GetAsync(BflThreadLocation location, CancellationToken token = default)
|
||||
@@ -87,7 +91,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
// The file stays as it is, see BeyondFactsLabStore.SaveThreadAsync:
|
||||
logger.LogWarning(exception, "Could not open the chat of the step '{Step}' of the preparation '{ProjectId}' of the BeyondFacts Lab.", location.Step, location.ProjectId);
|
||||
logger.LogWarning(exception, "Could not open the chat '{Location}' of the BeyondFacts Lab.", location.Key);
|
||||
thread = null;
|
||||
}
|
||||
|
||||
@@ -102,11 +106,18 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets the model open the conversation of a step, as the prompt of the step describes.
|
||||
/// Lets the model open a conversation, as its prompt describes.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <param name="provider">The provider which answers.</param>
|
||||
public Task StartAsync(BflThreadLocation location, AIStudio.Settings.Provider provider) => this.SendAsync(location, provider, OPENING_REQUEST, isHidden: true);
|
||||
public Task StartAsync(BflThreadLocation location, AIStudio.Settings.Provider provider) => this.SendAsync(location, provider, OpeningRequestFor(location), isHidden: true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request which lets the model open a conversation.
|
||||
/// </summary>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <returns>The request, which names the section of the prompt that describes the first message.</returns>
|
||||
public static string OpeningRequestFor(BflThreadLocation location) => location is BflOnboardingThreadLocation ? ONBOARDING_OPENING_REQUEST : OPENING_REQUEST;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message of the person and lets the model answer it.
|
||||
@@ -115,7 +126,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
/// The message is stored before the model answers, so that a crash loses at most the answer. What goes wrong is
|
||||
/// reported through the message bus; the method itself does not fail.
|
||||
/// </remarks>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <param name="provider">The provider which answers.</param>
|
||||
/// <param name="text">The message.</param>
|
||||
/// <param name="isHidden">Whether the person does not see the message, e.g., the request which opens a conversation.</param>
|
||||
@@ -125,7 +136,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
return;
|
||||
|
||||
var conversation = await this.GetAsync(location);
|
||||
if (conversation.Thread is not { } thread || !await this.CheckProviderAsync(thread, provider))
|
||||
if (conversation.Thread is not { } thread || !await this.CheckProviderAsync(location, thread, provider))
|
||||
return;
|
||||
|
||||
string systemPrompt;
|
||||
@@ -135,8 +146,8 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or FormatException)
|
||||
{
|
||||
logger.LogError(exception, "The BeyondFacts Lab could not put together the system prompt of the step '{Step}'.", location.Step);
|
||||
await messageBus.SendError(new(Icons.Material.Filled.ErrorOutline, TB("The BeyondFacts Lab could not put together its instructions for the model. Please report this as a bug.")));
|
||||
logger.LogError(exception, "The BeyondFacts Lab could not put together the system prompt of the chat '{Location}'.", location.Key);
|
||||
await messageBus.SendError(new(Icons.Material.Filled.ErrorOutline, TA("The BeyondFacts Lab could not put together its instructions for the model. Please report this as a bug.")));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,7 +219,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
logger.LogError(exception, "The BeyondFacts Lab could not store the chat of the step '{Step}'.", location.Step);
|
||||
logger.LogError(exception, "The BeyondFacts Lab could not store the chat '{Location}'.", location.Key);
|
||||
await messageBus.SendError(new(Icons.Material.Filled.SaveAs, TA("AI Studio could not save this conversation. Please try again.")));
|
||||
}
|
||||
finally
|
||||
@@ -218,16 +229,16 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the answer the model writes in a step.
|
||||
/// Stops the answer the model writes in a chat.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
public Task CancelAsync(BflThreadLocation location) => sessionService.CancelAsync(SessionKey(location));
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the conversation of a step, so that it starts over.
|
||||
/// Deletes a conversation, so that it starts over.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
/// <returns>False while the model answers in the step.</returns>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <returns>False while the model answers in the chat.</returns>
|
||||
public async Task<bool> ResetAsync(BflThreadLocation location)
|
||||
{
|
||||
var conversation = await this.GetAsync(location);
|
||||
@@ -253,7 +264,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
/// <returns>The IDs of the chats the preparation could have had.</returns>
|
||||
public async Task<IReadOnlyList<Guid>> ForgetProjectAsync(Guid projectId)
|
||||
{
|
||||
var locations = BflStep.All.Select(step => new BflThreadLocation(projectId, step)).ToList();
|
||||
var locations = BflStep.All.Select(step => new BflStepThreadLocation(projectId, step)).ToList();
|
||||
foreach (var location in locations)
|
||||
{
|
||||
await sessionService.CancelAsync(SessionKey(location));
|
||||
@@ -264,35 +275,47 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts together the system prompt of a step, anew for every request, so that changed details and a changed form
|
||||
/// Puts together the system prompt of a chat, anew for every request, so that changed details and a changed form
|
||||
/// of address count right away. The chat also uses it to count what the next request carries.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <returns>The system prompt.</returns>
|
||||
/// <exception cref="FileNotFoundException">The app comes without one of the prompts.</exception>
|
||||
/// <exception cref="FormatException">A prompt has a placeholder without a value.</exception>
|
||||
public async Task<string> ComposeSystemPromptAsync(BflThreadLocation location)
|
||||
{
|
||||
var forms = await store.LoadFormsAsync(location.ProjectId);
|
||||
var parts = await BflPromptLibrary.LoadPartsAsync(location.Step, Tools.PluginSystem.I18N.I.AddressForm);
|
||||
var addressForm = Tools.PluginSystem.I18N.I.AddressForm;
|
||||
var language = (await settingsManager.GetActiveLanguagePlugin()).LangName;
|
||||
return BflPromptComposer.Compose(parts, location.Step, forms, language);
|
||||
switch (location)
|
||||
{
|
||||
case BflStepThreadLocation stepLocation:
|
||||
var forms = await store.LoadFormsAsync(stepLocation.ProjectId);
|
||||
var parts = await BflPromptLibrary.LoadPartsAsync(stepLocation.Step, addressForm);
|
||||
return BflPromptComposer.Compose(parts, stepLocation.Step, forms, language);
|
||||
|
||||
case BflOnboardingThreadLocation:
|
||||
return BflPromptComposer.ComposeOnboarding(await BflPromptLibrary.LoadOnboardingPartsAsync(addressForm), language);
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(location), location, "The lab has no prompt for this chat.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a provider may answer in a chat, and tells the person why not.
|
||||
/// </summary>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
/// <param name="thread">The chat.</param>
|
||||
/// <param name="provider">The provider.</param>
|
||||
/// <returns>True when the provider may answer.</returns>
|
||||
private async Task<bool> CheckProviderAsync(ChatThread thread, AIStudio.Settings.Provider provider)
|
||||
private async Task<bool> CheckProviderAsync(BflThreadLocation location, ChatThread thread, AIStudio.Settings.Provider provider)
|
||||
{
|
||||
string? reason = null;
|
||||
if (provider.UsedLLMProvider is LLMProviders.NONE)
|
||||
reason = TA("Please select a provider.");
|
||||
else if (!settingsManager.IsProviderConfident(provider, Tools.Components.BEYOND_FACTS_LAB_ASSISTANT) || !thread.IsLLMProviderAllowed(provider))
|
||||
reason = TA("The selected provider may not see your preparation. Please select another provider.");
|
||||
else if (!BflModelRequirements.HasEnoughContext(provider.GetModelProfile().Context))
|
||||
else if (!BflModelRequirements.HasEnoughContext(location, provider.GetModelProfile().Context))
|
||||
reason = TA("The model of the selected provider reads too little at once for the lab. Please select another provider.");
|
||||
|
||||
if (reason is null)
|
||||
@@ -328,7 +351,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
logger.LogError(exception, "The BeyondFacts Lab could not store the chat of the step '{Step}'.", conversation.Location.Step);
|
||||
logger.LogError(exception, "The BeyondFacts Lab could not store the chat '{Location}'.", conversation.Location.Key);
|
||||
await messageBus.SendError(new(Icons.Material.Filled.SaveAs, TA("AI Studio could not save this conversation. Please try again.")));
|
||||
}
|
||||
}
|
||||
@@ -350,7 +373,7 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
/// <summary>
|
||||
/// Tells every page which listens that a chat changed.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the chat.</param>
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
private async Task NotifyAsync(BflThreadLocation location)
|
||||
{
|
||||
if (this.ConversationChanged is not { } handlers)
|
||||
@@ -371,8 +394,8 @@ public sealed class BflConversationService(BeyondFactsLabStore store, SettingsMa
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key under which a request of a step registers in AssistantSessionService.
|
||||
/// Gets the key under which a request of a chat registers in AssistantSessionService.
|
||||
/// </summary>
|
||||
/// <param name="location">The step of the preparation.</param>
|
||||
private static AssistantSessionKey SessionKey(BflThreadLocation location) => new(Tools.Components.BEYOND_FACTS_LAB_ASSISTANT, $"{location.ProjectId:D}/{location.Step.FolderName}");
|
||||
/// <param name="location">Where the chat belongs.</param>
|
||||
private static AssistantSessionKey SessionKey(BflThreadLocation location) => new(Tools.Components.BEYOND_FACTS_LAB_ASSISTANT, location.Key);
|
||||
}
|
||||
Reference in new issue
Block a user