diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 2352f7c3..a1e76f8c 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -89,6 +89,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher protected MudForm? form; protected bool inputIsValid; protected Profile currentProfile = Profile.NO_PROFILE; + protected ChatTemplate currentChatTemplate = ChatTemplate.NO_CHATTEMPLATE; protected ChatThread? chatThread; protected IContent? lastUserPrompt; protected CancellationTokenSource? cancellationTokenSource; @@ -115,6 +116,7 @@ public abstract partial class AssistantBase : AssistantLowerBase wher this.MightPreselectValues(); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); + this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component); } protected override async Task OnParametersSetAsync() diff --git a/app/MindWork AI Studio/Chat/ChatRole.cs b/app/MindWork AI Studio/Chat/ChatRole.cs index 339be971..d292b71b 100644 --- a/app/MindWork AI Studio/Chat/ChatRole.cs +++ b/app/MindWork AI Studio/Chat/ChatRole.cs @@ -1,3 +1,5 @@ +using AIStudio.Tools.PluginSystem; + namespace AIStudio.Chat; /// @@ -19,6 +21,8 @@ public enum ChatRole /// public static class ExtensionsChatRole { + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatRole).Namespace, nameof(ChatRole)); + /// /// Returns the name of the role. /// @@ -26,11 +30,11 @@ public static class ExtensionsChatRole /// The name of the role. public static string ToName(this ChatRole role) => role switch { - ChatRole.SYSTEM => "System", - ChatRole.USER => "You", - ChatRole.AI => "AI", + ChatRole.SYSTEM => TB("System"), + ChatRole.USER => TB("You"), + ChatRole.AI => TB("AI"), - _ => "Unknown", + _ => TB("Unknown"), }; /// @@ -60,4 +64,18 @@ public static class ExtensionsChatRole _ => Icons.Material.Filled.Help, }; + + /// + /// Returns the specific name of the role for the chat template. + /// + /// The role. + /// The name of the role. + public static string ToChatTemplateName(this ChatRole role) => role switch + { + ChatRole.SYSTEM => TB("System"), + ChatRole.USER => TB("User"), + ChatRole.AI => TB("Assistant"), + + _ => TB("Unknown"), + }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ChatRoles.cs b/app/MindWork AI Studio/Chat/ChatRoles.cs new file mode 100644 index 00000000..4c74aa14 --- /dev/null +++ b/app/MindWork AI Studio/Chat/ChatRoles.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Chat; + +public static class ChatRoles +{ + public static IEnumerable ChatTemplateRoles() + { + yield return ChatRole.USER; + yield return ChatRole.AI; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ChatThread.cs b/app/MindWork AI Studio/Chat/ChatThread.cs index ceb3041a..adb64850 100644 --- a/app/MindWork AI Studio/Chat/ChatThread.cs +++ b/app/MindWork AI Studio/Chat/ChatThread.cs @@ -29,6 +29,11 @@ public sealed record ChatThread /// Specifies the profile selected for the chat thread. /// public string SelectedProfile { get; set; } = string.Empty; + + /// + /// Specifies the profile selected for the chat thread. + /// + public string SelectedChatTemplate { get; set; } = string.Empty; /// /// The data source options for this chat thread. @@ -69,6 +74,8 @@ public sealed record ChatThread /// The content blocks of the chat thread. /// public List Blocks { get; init; } = []; + + private bool allowProfile = true; /// /// Prepares the system prompt for the chat thread. @@ -84,16 +91,52 @@ public sealed record ChatThread /// The prepared system prompt. public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger) { + // + // Use the information from the chat template, if provided. Otherwise, use the default system prompt + // + string systemPromptTextWithChatTemplate; + var logMessage = $"Using no chat template for chat thread '{chatThread.Name}'."; + if (string.IsNullOrWhiteSpace(chatThread.SelectedChatTemplate)) + systemPromptTextWithChatTemplate = chatThread.SystemPrompt; + else + { + if(!Guid.TryParse(chatThread.SelectedChatTemplate, out var chatTeamplateId)) + systemPromptTextWithChatTemplate = chatThread.SystemPrompt; + else + { + if(chatThread.SelectedChatTemplate == ChatTemplate.NO_CHATTEMPLATE.Id || chatTeamplateId == Guid.Empty) + systemPromptTextWithChatTemplate = chatThread.SystemPrompt; + else + { + var chatTemplate = settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatThread.SelectedChatTemplate); + if(chatTemplate == default) + systemPromptTextWithChatTemplate = chatThread.SystemPrompt; + else + { + logMessage = $"Using chat template '{chatTemplate.Name}' for chat thread '{chatThread.Name}'."; + this.allowProfile = chatTemplate.AllowProfileUsage; + systemPromptTextWithChatTemplate = $""" + {chatTemplate.ToSystemPrompt()} + """; + } + } + } + } + logger.LogInformation(logMessage); + + // + // Add augmented data, if available: + // var isAugmentedDataAvailable = !string.IsNullOrWhiteSpace(chatThread.AugmentedData); var systemPromptWithAugmentedData = isAugmentedDataAvailable switch { true => $""" - {chatThread.SystemPrompt} + {systemPromptTextWithChatTemplate} {chatThread.AugmentedData} """, - false => chatThread.SystemPrompt, + false => systemPromptTextWithChatTemplate, }; if(isAugmentedDataAvailable) @@ -101,12 +144,13 @@ public sealed record ChatThread else logger.LogInformation("No augmented data is available for the chat thread."); + // - // Prepare the system prompt: + // Add information from profile if available and allowed: // string systemPromptText; - var logMessage = $"Using no profile for chat thread '{chatThread.Name}'."; - if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile)) + logMessage = $"Using no profile for chat thread '{chatThread.Name}'."; + if ((string.IsNullOrWhiteSpace(chatThread.SelectedProfile)) || (this.allowProfile is false)) systemPromptText = systemPromptWithAugmentedData; else { diff --git a/app/MindWork AI Studio/Chat/ContentBlock.cs b/app/MindWork AI Studio/Chat/ContentBlock.cs index 632d98da..0568d597 100644 --- a/app/MindWork AI Studio/Chat/ContentBlock.cs +++ b/app/MindWork AI Studio/Chat/ContentBlock.cs @@ -18,15 +18,27 @@ public class ContentBlock /// /// The content of the block. /// - public IContent? Content { get; init; } + public IContent? Content { get; set; } /// /// The role of the content block in the chat thread, e.g., user, AI, etc. /// - public ChatRole Role { get; init; } = ChatRole.NONE; + public ChatRole Role { get; set; } = ChatRole.NONE; /// /// Should the content block be hidden from the user? /// public bool HideFromUser { get; set; } + + public ContentBlock DeepClone() + { + return new() + { + Time = this.Time, + ContentType = this.ContentType, + Content = this.Content?.DeepClone(), + Role = this.Role, + HideFromUser = this.HideFromUser, + }; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Chat/ContentImage.cs b/app/MindWork AI Studio/Chat/ContentImage.cs index c7e785eb..22eff248 100644 --- a/app/MindWork AI Studio/Chat/ContentImage.cs +++ b/app/MindWork AI Studio/Chat/ContentImage.cs @@ -32,6 +32,18 @@ public sealed class ContentImage : IContent, IImageSource { throw new NotImplementedException(); } + + /// + public IContent DeepClone() + { + return new ContentImage + { + Source = this.Source, + InitialRemoteWait = this.InitialRemoteWait, + IsStreaming = this.IsStreaming, + SourceType = this.SourceType, + }; + } #endregion diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index 38872edf..66166d7d 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -125,6 +125,17 @@ public sealed class ContentText : IContent return chatThread; } + /// + public IContent DeepClone() + { + return new ContentText + { + Text = this.Text, + InitialRemoteWait = this.InitialRemoteWait, + IsStreaming = this.IsStreaming, + }; + } + #endregion /// diff --git a/app/MindWork AI Studio/Chat/IContent.cs b/app/MindWork AI Studio/Chat/IContent.cs index be3bf097..c03f6574 100644 --- a/app/MindWork AI Studio/Chat/IContent.cs +++ b/app/MindWork AI Studio/Chat/IContent.cs @@ -43,6 +43,12 @@ public interface IContent /// public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default); + /// + /// Creates a deep copy + /// + /// The copy + public IContent DeepClone(); + /// /// Returns the corresponding ERI content type. /// diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 8973f9ac..0e1f5acf 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -80,6 +80,11 @@ } + + @if (this.SettingsManager.ConfigurationData.ChatTemplates.Count > 0) + { + + } @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) { @@ -106,9 +111,9 @@ } - - + + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) { diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index f01d88b1..4d3f08cf 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -46,6 +46,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private DataSourceSelection? dataSourceSelectionComponent; private DataSourceOptions earlyDataSourceOptions = new(); private Profile currentProfile = Profile.NO_PROFILE; + private ChatTemplate currentChatTemplate = ChatTemplate.NO_CHATTEMPLATE; private bool hasUnsavedChanges; private bool mustScrollToBottomAfterRender; private InnerScrolling scrollingArea = null!; @@ -59,7 +60,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private string currentWorkspaceName = string.Empty; private Guid currentWorkspaceId = Guid.Empty; private CancellationTokenSource? cancellationTokenSource; - + // Unfortunately, we need the input field reference to blur the focus away. Without // this, we cannot clear the input field. private MudTextField inputField = null!; @@ -77,6 +78,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Get the preselected profile: this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); + // Get the preselected chat template: + this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT); + // // Check for deferred messages of the kind 'SEND_TO_CHAT', // aka the user sends an assistant result to the chat: @@ -318,6 +322,15 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable await this.ChatThreadChanged.InvokeAsync(this.ChatThread); } + + private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate) + { + this.currentChatTemplate = chatTemplate; + if(this.ChatThread is null) + return; + + await this.StartNewChat(true, false); + } private IReadOnlyList GetAgentSelectedDataSources() { @@ -413,13 +426,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable { SelectedProvider = this.Provider.Id, SelectedProfile = this.currentProfile.Id, + SelectedChatTemplate = this.currentChatTemplate.Id, SystemPrompt = SystemPrompts.DEFAULT, WorkspaceId = this.currentWorkspaceId, ChatId = Guid.NewGuid(), DataSourceOptions = this.earlyDataSourceOptions, Name = this.ExtractThreadName(this.userInput), Seed = this.RNG.Next(), - Blocks = [], + Blocks = this.currentChatTemplate == default ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), }; await this.ChatThreadChanged.InvokeAsync(this.ChatThread); @@ -430,9 +444,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable if (string.IsNullOrWhiteSpace(this.ChatThread.Name)) this.ChatThread.Name = this.ExtractThreadName(this.userInput); - // Update provider and profile: + // Update provider, profile and chat template: this.ChatThread.SelectedProvider = this.Provider.Id; this.ChatThread.SelectedProfile = this.currentProfile.Id; + this.ChatThread.SelectedChatTemplate = this.currentChatTemplate.Id; } var time = DateTimeOffset.Now; @@ -643,12 +658,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable { SelectedProvider = this.Provider.Id, SelectedProfile = this.currentProfile.Id, + SelectedChatTemplate = this.currentChatTemplate.Id, SystemPrompt = SystemPrompts.DEFAULT, WorkspaceId = this.currentWorkspaceId, ChatId = Guid.NewGuid(), Name = string.Empty, Seed = this.RNG.Next(), - Blocks = [], + Blocks = this.currentChatTemplate == default ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(), }; } @@ -754,6 +770,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable { var chatProvider = this.ChatThread?.SelectedProvider; var chatProfile = this.ChatThread?.SelectedProfile; + var chatChatTemplate = this.ChatThread?.SelectedChatTemplate; switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior) { @@ -781,6 +798,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable if(this.currentProfile == default) this.currentProfile = Profile.NO_PROFILE; } + + // Try to select the chat template: + if (!string.IsNullOrWhiteSpace(chatChatTemplate)) + { + this.currentChatTemplate = this.SettingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == chatChatTemplate); + if(this.currentChatTemplate == default) + this.currentChatTemplate = ChatTemplate.NO_CHATTEMPLATE; + } } private async Task ToggleWorkspaceOverlay() diff --git a/app/MindWork AI Studio/Components/ChatTemplateSelection.razor b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor new file mode 100644 index 00000000..3fe83a3f --- /dev/null +++ b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor @@ -0,0 +1,20 @@ +@using AIStudio.Settings +@inherits MSGComponentBase + + + + + + @(this.CurrentChatTemplate != ChatTemplate.NO_CHATTEMPLATE ? this.CurrentChatTemplate.Name : "") + + + + @foreach (var chatTemplate in this.SettingsManager.ConfigurationData.ChatTemplates.GetAllChatTemplates()) + { + + @chatTemplate.Name + + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs new file mode 100644 index 00000000..141fed74 --- /dev/null +++ b/app/MindWork AI Studio/Components/ChatTemplateSelection.razor.cs @@ -0,0 +1,28 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class ChatTemplateSelection : MSGComponentBase +{ + [Parameter] + public ChatTemplate CurrentChatTemplate { get; set; } = ChatTemplate.NO_CHATTEMPLATE; + + [Parameter] + public EventCallback CurrentChatTemplateChanged { get; set; } + + [Parameter] + public string MarginLeft { get; set; } = "ml-1"; + + [Parameter] + public string MarginRight { get; set; } = string.Empty; + + private string MarginClass => $"{this.MarginLeft} {this.MarginRight}"; + + private async Task SelectionChanged(ChatTemplate chatTemplate) + { + this.CurrentChatTemplate = chatTemplate; + await this.CurrentChatTemplateChanged.InvokeAsync(chatTemplate); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor b/app/MindWork AI Studio/Components/ProfileSelection.razor index 9c9a0520..9a1e603b 100644 --- a/app/MindWork AI Studio/Components/ProfileSelection.razor +++ b/app/MindWork AI Studio/Components/ProfileSelection.razor @@ -1,6 +1,6 @@ @inherits MSGComponentBase - - + + @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) { diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs index efd15cb5..649e2140 100644 --- a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs @@ -1,11 +1,13 @@ using AIStudio.Settings; - +using AIStudio.Tools.PluginSystem; using Microsoft.AspNetCore.Components; namespace AIStudio.Components; public partial class ProfileSelection : MSGComponentBase { + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationProviderSelection).Namespace, nameof(ConfigurationProviderSelection)); + [Parameter] public Profile CurrentProfile { get; set; } = Profile.NO_PROFILE; @@ -18,6 +20,16 @@ public partial class ProfileSelection : MSGComponentBase [Parameter] public string MarginRight { get; set; } = string.Empty; + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string DisabledText { get; set; } = string.Empty; + + private readonly string defaultToolTipText = TB("You can switch between your profiles here"); + + private string ToolTipText => this.Disabled ? this.DisabledText : this.defaultToolTipText; + private string MarginClass => $"{this.MarginLeft} {this.MarginRight}"; private async Task SelectionChanged(Profile profile) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor new file mode 100644 index 00000000..2851f659 --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor @@ -0,0 +1,53 @@ +@inherits SettingsPanelBase + + + + @T("Your Chat Templates") + + + + @T("Customize your AI experience with our Chat Templates! Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to OpenAI's playground, you can define your own system prompts and leverage assistant prompts for providers that support them.") + + + + @T("Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.") + + + + + + + + + + # + @T("Chat Template Name") + @T("Actions") + + + @context.Num + @context.Name + + + + + + + + + + + + + + @if(this.SettingsManager.ConfigurationData.ChatTemplates.Count == 0) + { + + @T("No chat templates configured yet.") + + } + + + @T("Add Chat Template") + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor.cs new file mode 100644 index 00000000..ba494cb8 --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor.cs @@ -0,0 +1,73 @@ +using AIStudio.Dialogs; +using AIStudio.Settings; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Components.Settings; + +public partial class SettingsPanelChatTemplates : SettingsPanelBase +{ + private async Task AddChatTemplate() + { + var dialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Add Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var addedChatTemplate = (ChatTemplate)dialogResult.Data!; + addedChatTemplate = addedChatTemplate with { Num = this.SettingsManager.ConfigurationData.NextChatTemplateNum++ }; + + this.SettingsManager.ConfigurationData.ChatTemplates.Add(addedChatTemplate); + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task EditChatTemplate(ChatTemplate chatTemplate) + { + var dialogParameters = new DialogParameters + { + { x => x.DataNum, chatTemplate.Num }, + { x => x.DataId, chatTemplate.Id }, + { x => x.DataName, chatTemplate.Name }, + { x => x.DataSystemPrompt, chatTemplate.SystemPrompt }, + { x => x.IsEditing, true }, + {x => x.ExampleConversation, chatTemplate.ExampleConversation}, + {x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage}, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var editedChatTemplate = (ChatTemplate)dialogResult.Data!; + this.SettingsManager.ConfigurationData.ChatTemplates[this.SettingsManager.ConfigurationData.ChatTemplates.IndexOf(chatTemplate)] = editedChatTemplate; + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task DeleteChatTemplate(ChatTemplate chatTemplate) + { + var dialogParameters = new DialogParameters + { + { "Message", string.Format(T("Are you sure you want to delete the chat template '{0}'?"), chatTemplate.Name) }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Delete Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + this.SettingsManager.ConfigurationData.ChatTemplates.Remove(chatTemplate); + await this.SettingsManager.StoreSettings(); + + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor new file mode 100644 index 00000000..b00221ff --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor @@ -0,0 +1,128 @@ +@using AIStudio.Chat +@using MudBlazor.Extensions +@inherits MSGComponentBase +@inject ISnackbar Snackbar + + + + + + @T("Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements.") + + + + @T("The name of the chat template is mandatory. Each chat template must have a unique name.") + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + + @T("Use the default system prompt") + + + + + + + + @T("Example Conversation") + + + + @T("Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs.") + + + + + + + + + + @T("Role") + @T("Entry") + @T("Actions") + + + @context.Role.ToChatTemplateName() + + @(context.Content is ContentText textContent ? textContent.Text : context.Content?.ToString()) + + + + + + + + + + @foreach (var role in availableRoles) + { + @role.ToChatTemplateName() + } + + + + + + + + + + + + + @T("Add additional message") + + + + + + @T("Cancel") + + + @if(this.IsEditing) + { + @T("Update") + } + else + { + @T("Add") + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs new file mode 100644 index 00000000..a113d7a0 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs @@ -0,0 +1,224 @@ +using AIStudio.Chat; +using AIStudio.Components; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class ChatTemplateDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The chat template's number in the list. + /// + [Parameter] + public uint DataNum { get; set; } + + /// + /// The chat template's ID. + /// + [Parameter] + public string DataId { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The chat template name chosen by the user. + /// + [Parameter] + public string DataName { get; set; } = string.Empty; + + /// + /// What is the system prompt? + /// + [Parameter] + public string DataSystemPrompt { get; set; } = string.Empty; + + /// + /// Should the dialog be in editing mode? + /// + [Parameter] + public bool IsEditing { get; init; } + + [Parameter] + public List ExampleConversation { get; set; } = []; + + [Parameter] + public bool AllowProfileUsage { get; set; } = true; + + [Inject] + private ILogger Logger { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + /// + /// The list of used chat template names. We need this to check for uniqueness. + /// + private List UsedNames { get; set; } = []; + + private bool dataIsValid; + private string[] dataIssues = []; + private string dataEditingPreviousName = string.Empty; + + private ContentBlock messageEntryBeforeEdit; + private readonly IEnumerable availableRoles = ChatRoles.ChatTemplateRoles().ToArray(); + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + private ChatTemplate CreateChatTemplateSettings() => new() + { + Num = this.DataNum, + Id = this.DataId, + + Name = this.DataName, + SystemPrompt = this.DataSystemPrompt, + ExampleConversation = this.ExampleConversation, + AllowProfileUsage = this.AllowProfileUsage, + }; + + private void RemoveMessage(ContentBlock item) + { + this.ExampleConversation.Remove(item); + } + + private void AddNewMessageToEnd() + { + this.ExampleConversation ??= new List(); + + var newEntry = new ContentBlock + { + Role = ChatRole.USER, // Default to User + Content = new ContentText(), + ContentType = ContentType.TEXT, + HideFromUser = true, + Time = DateTimeOffset.Now, + }; + + this.ExampleConversation.Add(newEntry); + } + + private void AddNewMessageBelow(ContentBlock currentItem) + { + + // Create new entry with a valid role + var newEntry = new ContentBlock + { + Role = ChatRole.USER, // Default to User + Content = new ContentText(), + ContentType = ContentType.TEXT, + HideFromUser = true, + Time = DateTimeOffset.Now, + }; + + // Rest of the method remains the same + var index = this.ExampleConversation.IndexOf(currentItem); + + if (index >= 0) + { + this.ExampleConversation.Insert(index + 1, newEntry); + } + else + { + this.ExampleConversation.Add(newEntry); + } + } + + private void BackupItem(object element) + { + this.messageEntryBeforeEdit = new ContentBlock + { + Role = ((ContentBlock)element).Role, + Content = ((ContentBlock)element).Content, + }; + } + + private void ResetItemToOriginalValues(object element) + { + ((ContentBlock)element).Role = this.messageEntryBeforeEdit.Role; + ((ContentBlock)element).Content = this.messageEntryBeforeEdit.Content; + } + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + + // Load the used instance names: + this.UsedNames = this.SettingsManager.ConfigurationData.ChatTemplates.Select(x => x.Name.ToLowerInvariant()).ToList(); + + // When editing, we need to load the data: + if(this.IsEditing) + { + this.dataEditingPreviousName = this.DataName.ToLowerInvariant(); + } + + await base.OnInitializedAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Reset the validation when not editing and on the first render. + // We don't want to show validation errors when the user opens the dialog. + if(!this.IsEditing && firstRender) + this.form.ResetValidation(); + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + private async Task Store() + { + await this.form.Validate(); + + // When the data is not valid, we don't store it: + if (!this.dataIsValid) + return; + + // Use the data model to store the chat template. + // We just return this data to the parent component: + var addedChatTemplateSettings = this.CreateChatTemplateSettings(); + + if(this.IsEditing) + this.Logger.LogInformation($"Edited chat template '{addedChatTemplateSettings.Name}'."); + else + this.Logger.LogInformation($"Created chat template '{addedChatTemplateSettings.Name}'."); + + this.MudDialog.Close(DialogResult.Ok(addedChatTemplateSettings)); + } + + private string? ValidateSystemPrompt(string text) + { + if(text.Length > 444) + return T("The text must not exceed 444 characters."); + + return null; + } + + private string? ValidateName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return T("Please enter a name for the chat template."); + + if (name.Length > 40) + return T("The chat template name must not exceed 40 characters."); + + // The instance name must be unique: + var lowerName = name.ToLowerInvariant(); + if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName)) + return T("The chat template name must be unique; the chosen name is already in use."); + + return null; + } + + private void UseDefaultSystemPrompt() + { + this.DataSystemPrompt = SystemPrompts.DEFAULT; + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index 613a4d67..69bc8771 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -16,6 +16,7 @@ } + diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs new file mode 100644 index 00000000..39c1271f --- /dev/null +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -0,0 +1,38 @@ +using AIStudio.Chat; +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Settings; + +public readonly record struct ChatTemplate(uint Num, string Id, string Name, string SystemPrompt, List ExampleConversation, bool AllowProfileUsage) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); + + public static readonly ChatTemplate NO_CHATTEMPLATE = new() + { + Name = TB("Use no chat template"), + SystemPrompt = string.Empty, + Id = Guid.Empty.ToString(), + Num = uint.MaxValue, + ExampleConversation = [], + AllowProfileUsage = true, + }; + + #region Overrides of ValueType + + /// + /// Returns a string that represents the profile in a human-readable format. + /// + /// A string that represents the profile in a human-readable format. + public override string ToString() => this.Name; + + #endregion + + public string ToSystemPrompt() + { + if(this.Num == uint.MaxValue) + return string.Empty; + + return this.SystemPrompt; + } + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs index 4e461f9d..d14868fc 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectDataFactory.cs @@ -198,6 +198,12 @@ public static class ConfigurationSelectDataFactory yield return new(profile.Name, profile.Id); } + public static IEnumerable> GetChatTemplatesData(IEnumerable chatTemplates) + { + foreach (var chatTemplate in chatTemplates.GetAllChatTemplates()) + yield return new(chatTemplate.Name, chatTemplate.Id); + } + public static IEnumerable> GetConfidenceSchemesData() { foreach (var scheme in Enum.GetValues()) diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 439427bc..695d2ad8 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -35,6 +35,11 @@ public sealed class Data /// List of configured profiles. /// public List Profiles { get; init; } = []; + + /// + /// List of configured chat templates. + /// + public List ChatTemplates { get; init; } = []; /// /// List of enabled plugins. @@ -60,6 +65,11 @@ public sealed class Data /// The next profile number to use. /// public uint NextProfileNum { get; set; } = 1; + + /// + /// The next chat template number to use. + /// + public uint NextChatTemplateNum { get; set; } = 1; public DataApp App { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index af76fa26..e189cbbd 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -57,4 +57,10 @@ public sealed class DataApp /// Should we preselect a profile for the entire app? /// public string PreselectedProfile { get; set; } = string.Empty; + + + /// + /// Should we preselect a chat template for the entire app? + /// + public string PreselectedChatTemplate { get; set; } = string.Empty; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs index baf995fd..147bb7ac 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs @@ -37,6 +37,11 @@ public sealed class DataChat /// public string PreselectedProfile { get; set; } = string.Empty; + /// + /// Preselect a chat template? + /// + public string PreselectedChatTemplate { get; set; } = string.Empty; + /// /// Should we preselect data sources options for a created chat? /// diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index e34be596..e10e37bb 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -265,6 +265,16 @@ public sealed class SettingsManager preselection = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProfile); return preselection != default ? preselection : Profile.NO_PROFILE; } + + public ChatTemplate GetPreselectedChatTemplate(Tools.Components component) + { + var preselection = component.PreselectedChatTemplate(this); + if (preselection != default) + return preselection; + + preselection = this.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedChatTemplate); + return preselection != default ? preselection : ChatTemplate.NO_CHATTEMPLATE; + } public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider) { diff --git a/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs b/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs new file mode 100644 index 00000000..b7b2f183 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ChatTemplateExtensions.cs @@ -0,0 +1,13 @@ +using AIStudio.Settings; + +namespace AIStudio.Tools; + +public static class ChatTemplateExtensions +{ + public static IEnumerable GetAllChatTemplates(this IEnumerable chatTemplates) + { + yield return ChatTemplate.NO_CHATTEMPLATE; + foreach (var chatTemplate in chatTemplates) + yield return chatTemplate; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index f7e929c5..e4bd317c 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; - using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; @@ -131,4 +130,11 @@ public static class ComponentsExtensions _ => default, }; + + public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch + { + Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.ChatTemplates.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : default, + + _ => default, + }; } \ No newline at end of file