using AIStudio.Chat; using AIStudio.Chat.Embedded; using AIStudio.Components; using Microsoft.AspNetCore.Components; namespace AIStudio.Assistants.BeyondFactsLab; /// /// A chat of the BeyondFacts Lab, such as the one of a step below its form "Your details". /// /// /// The embedded chat of assistants with what the lab decides for each of its places: the opening request, the /// introduction, the question before starting over, and, for every request, the system prompt with the guide, the /// details, and the material, and the guidelines of the organization in front of the first message, see /// BflConversationService.PrepareAsync. The count of tokens shows how full the context /// of the model is, since every request carries the guide. It offers no "send to": the lab neither receives content /// from other assistants nor hands its answers on. /// public partial class BflChat : MSGComponentBase { /// /// Gets or sets where the chat to show belongs, e.g., to a step of a preparation. /// [Parameter, EditorRequired] public BflThreadLocation Location { get; set; } = null!; /// /// Gets or sets whether the chat shows a heading of its own; it needs none where its place already names it. /// [Parameter] public bool ShowHeading { get; set; } = true; /// /// Gets or sets the provider which answers. /// [Parameter, EditorRequired] public AIStudio.Settings.Provider Provider { get; set; } = Settings.Provider.NONE; /// /// Gets or sets why the provider cannot answer, or null when it can. /// [Parameter] public string? ProviderIssue { get; set; } [Inject] private BflConversationService ConversationService { get; init; } = null!; [Inject] private BflMaterialService MaterialService { get; init; } = null!; [Inject] private ILogger Logger { get; init; } = null!; /// /// Readies a request of the chat shown, with the provider chosen; made anew whenever either changes. /// private Func> prepareAsync = _ => Task.FromResult(EmbeddedChatPreparation.REFUSED); /// /// Gets what the next request of the chat shown carries beside the conversation, for the count of tokens; made anew /// whenever the chat or the provider changes. /// private Func> systemPromptForCountAsync = () => Task.FromResult(null); /// /// Changes whenever the material of the preparation changes, since every request of a step carries it. /// private int materialsVersion; /// /// Gets a number which changes whenever what the next request carries beside the conversation changed: the /// material, or the guidelines of the organization after a configuration plugin was reloaded. /// private int ContextVersion => HashCode.Combine(this.materialsVersion, this.SettingsManager.ConfigurationData.BeyondFactsLab.OrganizationRules); /// /// Gets what the chat says before its conversation begins. /// private string Introduction => this.Location switch { BflOnboardingThreadLocation => TA("Tell the lab what you are preparing for. It tells you whether it fits your task and how to work with it. You can also let the lab begin."), _ => TA("The lab opens the conversation of this step with what your details already say, and asks for what is missing. You can also write first.") }; /// /// Gets the name a message gets when the person exports it. /// private string ExportFileName => this.Location switch { BflStepThreadLocation stepLocation => stepLocation.Step.Title, _ => T("Is the lab right for my task?") }; /// /// Gets the question before the conversation starts over. /// private string ResetQuestion => this.Location switch { BflStepThreadLocation => TA("Delete the conversation of this step and start it over? Your details stay as they are."), _ => TA("Delete this conversation and start it over?") }; /// protected override async Task OnInitializedAsync() { this.MaterialService.MaterialsChanged += this.OnMaterialsChangedAsync; await base.OnInitializedAsync(); } /// protected override async Task OnParametersSetAsync() { await base.OnParametersSetAsync(); // Bound to the chat and the provider shown now, so that a request never takes those of the next render: var location = this.Location; var provider = this.Provider; this.prepareAsync = thread => this.ConversationService.PrepareAsync(location, provider, thread); this.systemPromptForCountAsync = () => this.SystemPromptForCountAsync(location, provider); } /// protected override void DisposeResources() { this.MaterialService.MaterialsChanged -= this.OnMaterialsChangedAsync; base.DisposeResources(); } /// /// Creates the chat of the place shown, for a place which has no stored chat yet. /// private ChatThread CreateThread() => BflThreadFactory.Create(this.Location); /// /// Lets the chat count again when the material of the preparation changed. /// /// The ID of the preparation whose material changed. private Task OnMaterialsChangedAsync(Guid projectId) { if (this.Location is not BflStepThreadLocation stepLocation || stepLocation.ProjectId != projectId) return Task.CompletedTask; return this.InvokeAsync(() => { this.materialsVersion++; this.StateHasChanged(); }); } /// /// Puts together what the next request of a chat carries beside the conversation, for the count of tokens: the /// system prompt, and the guidelines of the organization in front of the first message. /// /// Where the chat belongs. /// The provider which answers. /// The text to count, or null when it cannot be put together now. private async Task SystemPromptForCountAsync(BflThreadLocation location, AIStudio.Settings.Provider provider) { try { return await this.ConversationService.ComposeTextForCountAsync(location, provider); } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or FormatException) { // Sending reports the same problem; the count only stays unknown: this.Logger.LogDebug(exception, "Could not count the tokens of a chat in the BeyondFacts Lab."); return null; } } }