diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor index c87fbbb9..56d2b73c 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor @@ -201,6 +201,44 @@ Data retrieval settings + + For your ERI server, you need to retrieve data that matches a chat or prompt in some way. We call this the retrieval process. + You must describe at least one such process. You may offer several retrieval processes from which users can choose. This allows + you to test with beta users which process works better. Or you might generally want to give users the choice so they can select + the process that best suits their circumstances. + + + + + + + + + Name + Actions + + + @context.Name + + + Edit + + + Delete + + + + + +@if(this.retrievalProcesses.Count == 0) +{ + No retrieval process configured yet. +} + + + Add Retrieval Process + + You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs index 63d7dc0c..2effc79a 100644 --- a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -64,6 +64,7 @@ public partial class AssistantERI : AssistantBaseCore this.selectedOperatingSystem = OperatingSystem.NONE; this.allowedLLMProviders = AllowedLLMProviders.NONE; this.embeddings = new(); + this.retrievalProcesses = new(); this.additionalLibraries = string.Empty; } } @@ -92,6 +93,7 @@ public partial class AssistantERI : AssistantBaseCore this.selectedOperatingSystem = this.SettingsManager.ConfigurationData.ERI.PreselectedOperatingSystem; this.allowedLLMProviders = this.SettingsManager.ConfigurationData.ERI.PreselectedAllowedLLMProviders; this.embeddings = this.SettingsManager.ConfigurationData.ERI.PreselectedEmbeddingInfos; + this.retrievalProcesses = this.SettingsManager.ConfigurationData.ERI.PreselectedRetrievalInfos; this.additionalLibraries = this.SettingsManager.ConfigurationData.ERI.PreselectedAdditionalLibraries; return true; } @@ -125,6 +127,7 @@ public partial class AssistantERI : AssistantBaseCore this.SettingsManager.ConfigurationData.ERI.PreselectedOperatingSystem = this.selectedOperatingSystem; this.SettingsManager.ConfigurationData.ERI.PreselectedAllowedLLMProviders = this.allowedLLMProviders; this.SettingsManager.ConfigurationData.ERI.PreselectedEmbeddingInfos = this.embeddings; + this.SettingsManager.ConfigurationData.ERI.PreselectedRetrievalInfos = this.retrievalProcesses; this.SettingsManager.ConfigurationData.ERI.PreselectedAdditionalLibraries = this.additionalLibraries; await this.SettingsManager.StoreSettings(); } @@ -146,6 +149,7 @@ public partial class AssistantERI : AssistantBaseCore private OperatingSystem selectedOperatingSystem = OperatingSystem.NONE; private AllowedLLMProviders allowedLLMProviders = AllowedLLMProviders.NONE; private List embeddings = new(); + private List retrievalProcesses = new(); private string additionalLibraries = string.Empty; private string? ValidateServerName(string name) @@ -460,6 +464,65 @@ public partial class AssistantERI : AssistantBaseCore this.embeddings.Remove(embeddingInfo); await this.AutoSave(); } + + private async Task AddRetrievalProcess() + { + var dialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + { x => x.AvailableEmbeddings, this.embeddings }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Add Retrieval Process", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var addedRetrievalProcess = (RetrievalInfo)dialogResult.Data!; + this.retrievalProcesses.Add(addedRetrievalProcess); + await this.AutoSave(); + } + + private async Task EditRetrievalProcess(RetrievalInfo retrievalInfo) + { + var dialogParameters = new DialogParameters + { + { x => x.DataName, retrievalInfo.Name }, + { x => x.DataDescription, retrievalInfo.Description }, + { x => x.DataLink, retrievalInfo.Link }, + { x => x.DataParametersDescription, retrievalInfo.ParametersDescription }, + { x => x.DataEmbeddings, retrievalInfo.Embeddings?.ToHashSet() }, + + { x => x.IsEditing, true }, + { x => x.AvailableEmbeddings, this.embeddings }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Edit Retrieval Process", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var editedRetrievalProcess = (RetrievalInfo)dialogResult.Data!; + + this.retrievalProcesses[this.retrievalProcesses.IndexOf(retrievalInfo)] = editedRetrievalProcess; + await this.AutoSave(); + } + + private async Task DeleteRetrievalProcess(RetrievalInfo retrievalInfo) + { + var dialogParameters = new DialogParameters + { + { "Message", $"Are you sure you want to delete the retrieval process '{retrievalInfo.Name}'?" }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Delete Retrieval Process", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + this.retrievalProcesses.Remove(retrievalInfo); + await this.AutoSave(); + } private async Task GenerateServer() { diff --git a/app/MindWork AI Studio/Assistants/ERI/RetrievalInfo.cs b/app/MindWork AI Studio/Assistants/ERI/RetrievalInfo.cs new file mode 100644 index 00000000..78ff7d2f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/RetrievalInfo.cs @@ -0,0 +1,18 @@ +namespace AIStudio.Assistants.ERI; + +/// +/// Information about a retrieval process, which this data source implements. +/// +/// The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval". +/// A short description of the retrieval process. What kind of retrieval process is it? +/// A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null. +/// A dictionary that describes the parameters of the retrieval process. The key is the parameter name, +/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the +/// expected type and range, e.g., 0.0 to 1.0 for a float parameter. +/// A list of embeddings used in this retrieval process. It might be empty in case no embedding is used. +public readonly record struct RetrievalInfo( + string Name, + string Description, + string? Link, + Dictionary? ParametersDescription, + List? Embeddings); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/RetrievalParameter.cs b/app/MindWork AI Studio/Assistants/ERI/RetrievalParameter.cs new file mode 100644 index 00000000..d25a5bdd --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/RetrievalParameter.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Assistants.ERI; + +public sealed class RetrievalParameter +{ + /// + /// The name of the parameter. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The description of the parameter. + /// + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor new file mode 100644 index 00000000..fb7e3b00 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor @@ -0,0 +1,213 @@ +@using AIStudio.Assistants.ERI +@using MudExtensions + + + + + + General Information + + + + Please provide some general information about your retrieval process first. This data may be + displayed to the users. + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + Retrieval Process Parameters + + + + You may want to parameterize your retrieval process. However, this is optional. You can specify any + parameters that can be set by the user or the system during the call. Nevertheless, you should use + sensible default values in your code so that users are not forced to set the parameters manually. + + + + @* The left side of the stack is another stack to show the list *@ + + @if (this.retrievalParameters.Count > 0) + { + + @foreach (var parameter in this.retrievalParameters) + { + + @parameter.Name + + } + + } + + Add Parameter + + + + @* The right side of the stack is another stack to display the parameter's data *@ + + @if (this.selectedParameter is null) + { + @if(this.retrievalParameters.Count == 0) + { + + Add a parameter first, then select it to edit. + + } + else + { + + Select a parameter to show and edit it. + + } + } + else + { + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + Delete this parameter + + + + } + + + + + Embeddings + + + @if(this.AvailableEmbeddings.Count == 0) + { + + Currently, you have not defined any embedding methods. If your retrieval process does not require embedding, you can ignore this part. + Otherwise, you can define one or more embedding methods in the previous view to assign them to your retrieval process here. + + } + else + { + + Here you can select which embedding methods are used for this retrieval process. Embeddings are optional; + if your retrieval process works without embedding, you can ignore this part. You can only choose the embedding + methods you have previously defined. + + + + @foreach (var embedding in this.AvailableEmbeddings) + { + + @embedding.EmbeddingName + + } + + } + + + + + Cancel + + @if(this.IsEditing) + { + @:Update + } + else + { + @:Add + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs new file mode 100644 index 00000000..371677b0 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs @@ -0,0 +1,204 @@ +using AIStudio.Assistants.ERI; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class RetrievalProcessDialog : ComponentBase +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The user chosen retrieval process name. + /// + [Parameter] + public string DataName { get; set; } = string.Empty; + + /// + /// The retrieval process description. + /// + [Parameter] + public string DataDescription { get; set; } = string.Empty; + + /// + /// A link to the retrieval process documentation, paper, Wikipedia article, or the source code. + /// + [Parameter] + public string DataLink { get; set; } = string.Empty; + + /// + /// A dictionary that describes the parameters of the retrieval process. The key is the parameter name, + /// and the value is a description of the parameter. Although each parameter will be sent as a string, + /// the description should indicate the expected type and range, e.g., 0.0 to 1.0 for a float parameter. + /// + [Parameter] + public Dictionary DataParametersDescription { get; set; } = new(); + + /// + /// A list of embeddings used in this retrieval process. It might be empty in case no embedding is used. + /// + [Parameter] + public HashSet DataEmbeddings { get; set; } = new(); + + /// + /// The available embeddings for the user to choose from. + /// + [Parameter] + public IReadOnlyList AvailableEmbeddings { get; set; } = new List(); + + /// + /// Should the dialog be in editing mode? + /// + [Parameter] + public bool IsEditing { get; init; } + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + private bool dataIsValid; + private string[] dataIssues = []; + private List retrievalParameters = new(); + private RetrievalParameter? selectedParameter; + private uint nextParameterId = 1; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + private RetrievalInfo CreateRetrievalInfo() => new(this.DataName, this.DataDescription, this.DataLink, this.retrievalParameters.ToDictionary(parameter => parameter.Name, parameter => parameter.Description), this.DataEmbeddings.ToList()); + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + + // Convert the parameters: + this.retrievalParameters = this.DataParametersDescription.Select(pair => new RetrievalParameter { Name = pair.Key, Description = pair.Value }).ToList(); + + 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 string? ValidateName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return "The retrieval process name must not be empty. Please name your retrieval process."; + + if (name.Length > 26) + return "The retrieval process name must not be longer than 26 characters."; + + return null; + } + + private string? ValidateDescription(string description) + { + if (string.IsNullOrWhiteSpace(description)) + return "The description must not be empty. Please describe the retrieval process."; + + return null; + } + + private void AddRetrievalProcessParameter() + { + this.retrievalParameters.Add(new() { Name = $"New Parameter {this.nextParameterId++}", Description = string.Empty }); + } + + private string? ValidateParameterName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return "The parameter name must not be empty. Please name the parameter."; + + if(name.Length > 26) + return "The parameter name must not be longer than 26 characters."; + + if (this.retrievalParameters.Count(parameter => parameter.Name == name) > 1) + return $"The parameter name '{name}' must be unique. Please choose a different name."; + + return null; + } + + private string? ValidateParameterDescription(string description) + { + if (string.IsNullOrWhiteSpace(description)) + return $"The parameter description must not be empty. Please describe the parameter '{this.selectedParameter?.Name}'. What data type is it? What is it used for? What are the possible values?"; + + return null; + } + + private string? ValidateParameter(RetrievalParameter parameter) + { + if(this.ValidateParameterName(parameter.Name) is { } nameIssue) + return nameIssue; + + if (string.IsNullOrWhiteSpace(parameter.Description)) + return $"The parameter description must not be empty. Please describe the parameter '{parameter.Name}'. What data type is it? What is it used for? What are the possible values?"; + + return null; + } + + private void RemoveRetrievalProcessParameter() + { + if (this.selectedParameter is not null) + this.retrievalParameters.Remove(this.selectedParameter); + + this.selectedParameter = null; + } + + private string GetMultiSelectionText(List selectedEmbeddings) + { + if(selectedEmbeddings.Count == 0) + return "No embedding methods selected."; + + if(selectedEmbeddings.Count == 1) + return "You have selected 1 embedding method."; + + return $"You have selected {selectedEmbeddings.Count} embedding methods."; + } + + private void EmbeddingsChanged(IEnumerable? updatedEmbeddings) + { + if(updatedEmbeddings is null) + this.DataEmbeddings = new(); + else + this.DataEmbeddings = updatedEmbeddings.ToHashSet(); + } + + private async Task Store() + { + await this.form.Validate(); + foreach (var parameter in this.retrievalParameters) + { + if (this.ValidateParameter(parameter) is { } issue) + { + this.dataIsValid = false; + Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1); + this.dataIssues[^1] = issue; + } + } + + // When the data is not valid, we don't store it: + if (!this.dataIsValid || this.dataIssues.Any()) + return; + + var retrievalInfo = this.CreateRetrievalInfo(); + this.MudDialog.Close(DialogResult.Ok(retrievalInfo)); + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataERI.cs b/app/MindWork AI Studio/Settings/DataModel/DataERI.cs index 1d58f125..4949ab14 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataERI.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataERI.cs @@ -98,7 +98,10 @@ public sealed class DataERI public List PreselectedEmbeddingInfos { get; set; } = new(); /// + /// Do you want to predefine any retrieval information? /// + public List PreselectedRetrievalInfos { get; set; } = new(); + /// /// Do you want to preselect any additional libraries? ///