Files
AI-Studio/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs
T

1015 lines
45 KiB
C#

using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants.ERI;
public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
{
[Inject]
private WebRootFiles WebRootFiles { get; set; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
protected override Tools.Components Component => Tools.Components.ERI_ASSISTANT;
protected override string Title => T("ERI Server");
protected override string Description => T("The ERI is the External Retrieval Interface for AI Studio and other tools. The ERI acts as a contract between decentralized data sources and, e.g., AI Studio. The ERI is implemented by the data sources, allowing them to be integrated into AI Studio later. This means that the data sources assume the server role and AI Studio (or any other LLM tool) assumes the client role of the API. This approach serves to realize a Retrieval-Augmented Generation (RAG) process with external data.");
protected override string SystemPrompt => new ERIServerPrompt
{
ProgrammingLanguage = this.selectedProgrammingLanguage,
OtherProgrammingLanguage = this.otherProgrammingLanguage,
ERISpecification = this.eriSpecification,
ServerName = this.serverName,
ServerDescription = this.serverDescription,
DataSource = this.selectedDataSource,
OtherDataSource = this.otherDataSource,
DataSourceProductName = this.dataSourceProductName,
DataSourceHostname = this.dataSourceHostname,
DataSourcePort = this.dataSourcePort,
AuthenticationMethods = this.selectedAuthenticationMethods,
AuthDescription = this.authDescription,
OperatingSystem = this.selectedOperatingSystem,
AllowedLLMProviders = this.allowedLLMProviders,
RetrievalProcesses = this.retrievalProcesses,
AdditionalLibraries = this.additionalLibraries,
}.ToSystemPrompt();
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new ButtonData
{
Text = T("Open in chat"),
Icon = Icons.Material.Filled.Chat,
Color = Color.Default,
AsyncAction = this.OpenInChat,
DisabledActionParam = () => !this.CanOpenInChat,
},
];
protected override bool ShowEntireChatThread => true;
protected override bool ShowSendTo => false;
protected override string SubmitText => T("Create the ERI server");
protected override Func<Task> SubmitAction => this.GenerateServer;
protected override bool SubmitDisabled => this.IsNoneERIServerSelected;
protected override ChatThread ConvertToChatThread => (this.ChatThread ?? new()) with
{
SystemPrompt = this.SystemPrompt,
};
/// <summary>
/// Indicates whether the generated ERI conversation can be opened in the chat view.
/// </summary>
private bool CanOpenInChat => !this.IsProcessing && this.ChatThread is { Blocks.Count: > 0 };
/// <summary>
/// Opens the generated ERI conversation in the chat view when a finished conversation is available.
/// </summary>
private async Task OpenInChat()
{
if (!this.CanOpenInChat)
return;
await this.SendToAssistant(Tools.Components.CHAT, default);
}
protected override void ResetForm()
{
if (!this.MightPreselectValues())
{
this.serverName = string.Empty;
this.serverDescription = string.Empty;
this.selectedERIVersion = ERIVersion.V1;
this.selectedProgrammingLanguage = ProgrammingLanguages.NONE;
this.otherProgrammingLanguage = string.Empty;
this.selectedDataSource = DataSources.NONE;
this.dataSourceProductName = string.Empty;
this.otherDataSource = string.Empty;
this.dataSourceHostname = string.Empty;
this.dataSourcePort = null;
this.userTypedPort = false;
this.selectedAuthenticationMethods = [];
this.authDescription = string.Empty;
this.selectedOperatingSystem = OperatingSystem.NONE;
this.allowedLLMProviders = AllowedLLMProviders.NONE;
this.embeddings = new();
this.retrievalProcesses = new();
this.additionalLibraries = string.Empty;
this.writeToFilesystem = false;
this.baseDirectory = string.Empty;
this.previouslyGeneratedFiles = new();
}
}
protected override bool MightPreselectValues()
{
this.autoSave = this.SettingsManager.ConfigurationData.ERI.AutoSaveChanges;
if (this.SettingsManager.ConfigurationData.ERI.PreselectOptions && this.selectedERIServer is not null)
{
this.serverName = this.selectedERIServer.ServerName;
this.serverDescription = this.selectedERIServer.ServerDescription;
this.selectedERIVersion = this.selectedERIServer.ERIVersion;
this.selectedProgrammingLanguage = this.selectedERIServer.ProgrammingLanguage;
this.otherProgrammingLanguage = this.selectedERIServer.OtherProgrammingLanguage;
this.selectedDataSource = this.selectedERIServer.DataSource;
this.dataSourceProductName = this.selectedERIServer.DataSourceProductName;
this.otherDataSource = this.selectedERIServer.OtherDataSource;
this.dataSourceHostname = this.selectedERIServer.DataSourceHostname;
this.dataSourcePort = this.selectedERIServer.DataSourcePort;
this.userTypedPort = this.selectedERIServer.UserTypedPort;
var authMethods = new HashSet<Auth>(this.selectedERIServer.AuthMethods);
this.selectedAuthenticationMethods = authMethods;
this.authDescription = this.selectedERIServer.AuthDescription;
this.selectedOperatingSystem = this.selectedERIServer.OperatingSystem;
this.allowedLLMProviders = this.selectedERIServer.AllowedLLMProviders;
this.embeddings = this.selectedERIServer.EmbeddingInfos;
this.retrievalProcesses = this.selectedERIServer.RetrievalInfos;
this.additionalLibraries = this.selectedERIServer.AdditionalLibraries;
this.writeToFilesystem = this.selectedERIServer.WriteToFilesystem;
this.baseDirectory = this.selectedERIServer.BaseDirectory;
this.previouslyGeneratedFiles = this.selectedERIServer.PreviouslyGeneratedFiles;
return true;
}
return false;
}
protected override async Task OnFormChange()
{
await this.AutoSave();
}
#region Overrides of AssistantBase
protected override async Task OnInitializedAsync()
{
this.selectedERIServer = this.SettingsManager.ConfigurationData.ERI.ERIServers.FirstOrDefault();
if(this.selectedERIServer is null)
{
await this.AddERIServer();
this.selectedERIServer = this.SettingsManager.ConfigurationData.ERI.ERIServers.First();
}
await base.OnInitializedAsync();
}
#endregion
private async Task AutoSave()
{
if(!this.autoSave || !this.SettingsManager.ConfigurationData.ERI.PreselectOptions)
return;
if(this.selectedERIServer is null)
return;
this.SettingsManager.ConfigurationData.ERI.PreselectedProvider = this.ProviderSettings.Id;
this.selectedERIServer.ServerName = this.serverName;
this.selectedERIServer.ServerDescription = this.serverDescription;
this.selectedERIServer.ERIVersion = this.selectedERIVersion;
this.selectedERIServer.ProgrammingLanguage = this.selectedProgrammingLanguage;
this.selectedERIServer.OtherProgrammingLanguage = this.otherProgrammingLanguage;
this.selectedERIServer.DataSource = this.selectedDataSource;
this.selectedERIServer.DataSourceProductName = this.dataSourceProductName;
this.selectedERIServer.OtherDataSource = this.otherDataSource;
this.selectedERIServer.DataSourceHostname = this.dataSourceHostname;
this.selectedERIServer.DataSourcePort = this.dataSourcePort;
this.selectedERIServer.UserTypedPort = this.userTypedPort;
this.selectedERIServer.AuthMethods = [..this.selectedAuthenticationMethods];
this.selectedERIServer.AuthDescription = this.authDescription;
this.selectedERIServer.OperatingSystem = this.selectedOperatingSystem;
this.selectedERIServer.AllowedLLMProviders = this.allowedLLMProviders;
this.selectedERIServer.EmbeddingInfos = this.embeddings;
this.selectedERIServer.RetrievalInfos = this.retrievalProcesses;
this.selectedERIServer.AdditionalLibraries = this.additionalLibraries;
this.selectedERIServer.WriteToFilesystem = this.writeToFilesystem;
this.selectedERIServer.BaseDirectory = this.baseDirectory;
this.selectedERIServer.PreviouslyGeneratedFiles = this.previouslyGeneratedFiles;
await this.SettingsManager.StoreSettings();
}
private DataERIServer? selectedERIServer;
private bool autoSave;
private string serverName = string.Empty;
private string serverDescription = string.Empty;
private ERIVersion selectedERIVersion = ERIVersion.V1;
private string? eriSpecification;
private ProgrammingLanguages selectedProgrammingLanguage = ProgrammingLanguages.NONE;
private string otherProgrammingLanguage = string.Empty;
private DataSources selectedDataSource = DataSources.NONE;
private string otherDataSource = string.Empty;
private string dataSourceProductName = string.Empty;
private string dataSourceHostname = string.Empty;
private int? dataSourcePort;
private bool userTypedPort;
private IEnumerable<Auth> selectedAuthenticationMethods = new HashSet<Auth>();
private string authDescription = string.Empty;
private OperatingSystem selectedOperatingSystem = OperatingSystem.NONE;
private AllowedLLMProviders allowedLLMProviders = AllowedLLMProviders.NONE;
private List<EmbeddingInfo> embeddings = new();
private List<RetrievalInfo> retrievalProcesses = new();
private string additionalLibraries = string.Empty;
private bool writeToFilesystem;
private string baseDirectory = string.Empty;
private List<string> previouslyGeneratedFiles = new();
private static readonly AssistantSessionStateKey<DataERIServer?> SELECTED_ERI_SERVER_STATE_KEY = new(nameof(selectedERIServer));
private static readonly AssistantSessionStateKey<bool> AUTO_SAVE_STATE_KEY = new(nameof(autoSave));
private static readonly AssistantSessionStateKey<string> SERVER_NAME_STATE_KEY = new(nameof(serverName));
private static readonly AssistantSessionStateKey<string> SERVER_DESCRIPTION_STATE_KEY = new(nameof(serverDescription));
private static readonly AssistantSessionStateKey<ERIVersion> SELECTED_ERI_VERSION_STATE_KEY = new(nameof(selectedERIVersion));
private static readonly AssistantSessionStateKey<string?> ERI_SPECIFICATION_STATE_KEY = new(nameof(eriSpecification));
private static readonly AssistantSessionStateKey<ProgrammingLanguages> SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY = new(nameof(selectedProgrammingLanguage));
private static readonly AssistantSessionStateKey<string> OTHER_PROGRAMMING_LANGUAGE_STATE_KEY = new(nameof(otherProgrammingLanguage));
private static readonly AssistantSessionStateKey<DataSources> SELECTED_DATA_SOURCE_STATE_KEY = new(nameof(selectedDataSource));
private static readonly AssistantSessionStateKey<string> OTHER_DATA_SOURCE_STATE_KEY = new(nameof(otherDataSource));
private static readonly AssistantSessionStateKey<string> DATA_SOURCE_PRODUCT_NAME_STATE_KEY = new(nameof(dataSourceProductName));
private static readonly AssistantSessionStateKey<string> DATA_SOURCE_HOSTNAME_STATE_KEY = new(nameof(dataSourceHostname));
private static readonly AssistantSessionStateKey<int?> DATA_SOURCE_PORT_STATE_KEY = new(nameof(dataSourcePort));
private static readonly AssistantSessionStateKey<bool> USER_TYPED_PORT_STATE_KEY = new(nameof(userTypedPort));
private static readonly AssistantSessionStateKey<HashSet<Auth>> SELECTED_AUTHENTICATION_METHODS_STATE_KEY = new(nameof(selectedAuthenticationMethods));
private static readonly AssistantSessionStateKey<string> AUTH_DESCRIPTION_STATE_KEY = new(nameof(authDescription));
private static readonly AssistantSessionStateKey<OperatingSystem> SELECTED_OPERATING_SYSTEM_STATE_KEY = new(nameof(selectedOperatingSystem));
private static readonly AssistantSessionStateKey<AllowedLLMProviders> ALLOWED_LLM_PROVIDERS_STATE_KEY = new(nameof(allowedLLMProviders));
private static readonly AssistantSessionStateKey<List<EmbeddingInfo>> EMBEDDINGS_STATE_KEY = new(nameof(embeddings));
private static readonly AssistantSessionStateKey<List<RetrievalInfo>> RETRIEVAL_PROCESSES_STATE_KEY = new(nameof(retrievalProcesses));
private static readonly AssistantSessionStateKey<string> ADDITIONAL_LIBRARIES_STATE_KEY = new(nameof(additionalLibraries));
private static readonly AssistantSessionStateKey<bool> WRITE_TO_FILESYSTEM_STATE_KEY = new(nameof(writeToFilesystem));
private static readonly AssistantSessionStateKey<string> BASE_DIRECTORY_STATE_KEY = new(nameof(baseDirectory));
private static readonly AssistantSessionStateKey<List<string>> PREVIOUSLY_GENERATED_FILES_STATE_KEY = new(nameof(previouslyGeneratedFiles));
/// <inheritdoc />
protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state)
{
state.Set(SELECTED_ERI_SERVER_STATE_KEY, this.selectedERIServer);
state.Set(AUTO_SAVE_STATE_KEY, this.autoSave);
state.Set(SERVER_NAME_STATE_KEY, this.serverName);
state.Set(SERVER_DESCRIPTION_STATE_KEY, this.serverDescription);
state.Set(SELECTED_ERI_VERSION_STATE_KEY, this.selectedERIVersion);
state.Set(ERI_SPECIFICATION_STATE_KEY, this.eriSpecification);
state.Set(SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY, this.selectedProgrammingLanguage);
state.Set(OTHER_PROGRAMMING_LANGUAGE_STATE_KEY, this.otherProgrammingLanguage);
state.Set(SELECTED_DATA_SOURCE_STATE_KEY, this.selectedDataSource);
state.Set(OTHER_DATA_SOURCE_STATE_KEY, this.otherDataSource);
state.Set(DATA_SOURCE_PRODUCT_NAME_STATE_KEY, this.dataSourceProductName);
state.Set(DATA_SOURCE_HOSTNAME_STATE_KEY, this.dataSourceHostname);
state.Set(DATA_SOURCE_PORT_STATE_KEY, this.dataSourcePort);
state.Set(USER_TYPED_PORT_STATE_KEY, this.userTypedPort);
state.SetHashSet(SELECTED_AUTHENTICATION_METHODS_STATE_KEY, this.selectedAuthenticationMethods);
state.Set(AUTH_DESCRIPTION_STATE_KEY, this.authDescription);
state.Set(SELECTED_OPERATING_SYSTEM_STATE_KEY, this.selectedOperatingSystem);
state.Set(ALLOWED_LLM_PROVIDERS_STATE_KEY, this.allowedLLMProviders);
state.SetList(EMBEDDINGS_STATE_KEY, this.embeddings);
state.SetList(RETRIEVAL_PROCESSES_STATE_KEY, this.retrievalProcesses);
state.Set(ADDITIONAL_LIBRARIES_STATE_KEY, this.additionalLibraries);
state.Set(WRITE_TO_FILESYSTEM_STATE_KEY, this.writeToFilesystem);
state.Set(BASE_DIRECTORY_STATE_KEY, this.baseDirectory);
state.SetList(PREVIOUSLY_GENERATED_FILES_STATE_KEY, this.previouslyGeneratedFiles);
}
/// <inheritdoc />
protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state)
{
state.Restore(SELECTED_ERI_SERVER_STATE_KEY, value => this.selectedERIServer = value);
state.Restore(AUTO_SAVE_STATE_KEY, value => this.autoSave = value);
state.Restore(SERVER_NAME_STATE_KEY, value => this.serverName = value);
state.Restore(SERVER_DESCRIPTION_STATE_KEY, value => this.serverDescription = value);
state.Restore(SELECTED_ERI_VERSION_STATE_KEY, value => this.selectedERIVersion = value);
state.Restore(ERI_SPECIFICATION_STATE_KEY, value => this.eriSpecification = value);
state.Restore(SELECTED_PROGRAMMING_LANGUAGE_STATE_KEY, value => this.selectedProgrammingLanguage = value);
state.Restore(OTHER_PROGRAMMING_LANGUAGE_STATE_KEY, value => this.otherProgrammingLanguage = value);
state.Restore(SELECTED_DATA_SOURCE_STATE_KEY, value => this.selectedDataSource = value);
state.Restore(OTHER_DATA_SOURCE_STATE_KEY, value => this.otherDataSource = value);
state.Restore(DATA_SOURCE_PRODUCT_NAME_STATE_KEY, value => this.dataSourceProductName = value);
state.Restore(DATA_SOURCE_HOSTNAME_STATE_KEY, value => this.dataSourceHostname = value);
state.Restore(DATA_SOURCE_PORT_STATE_KEY, value => this.dataSourcePort = value);
state.Restore(USER_TYPED_PORT_STATE_KEY, value => this.userTypedPort = value);
state.Restore(SELECTED_AUTHENTICATION_METHODS_STATE_KEY, value => this.selectedAuthenticationMethods = value);
state.Restore(AUTH_DESCRIPTION_STATE_KEY, value => this.authDescription = value);
state.Restore(SELECTED_OPERATING_SYSTEM_STATE_KEY, value => this.selectedOperatingSystem = value);
state.Restore(ALLOWED_LLM_PROVIDERS_STATE_KEY, value => this.allowedLLMProviders = value);
state.RestoreList(EMBEDDINGS_STATE_KEY, this.embeddings);
state.RestoreList(RETRIEVAL_PROCESSES_STATE_KEY, this.retrievalProcesses);
state.Restore(ADDITIONAL_LIBRARIES_STATE_KEY, value => this.additionalLibraries = value);
state.Restore(WRITE_TO_FILESYSTEM_STATE_KEY, value => this.writeToFilesystem = value);
state.Restore(BASE_DIRECTORY_STATE_KEY, value => this.baseDirectory = value);
state.RestoreList(PREVIOUSLY_GENERATED_FILES_STATE_KEY, this.previouslyGeneratedFiles);
}
private bool AreServerPresetsBlocked => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions;
/// <summary>
/// Gets whether ERI server preset controls should be disabled.
/// </summary>
private bool AreServerPresetControlsDisabled => this.AreServerPresetsBlocked || this.IsProcessing;
private void SelectedERIServerChanged(DataERIServer? server)
{
if (this.IsProcessing)
return;
this.selectedERIServer = server;
this.ResetForm();
}
private async Task AddERIServer()
{
if (this.IsProcessing)
return;
this.SettingsManager.ConfigurationData.ERI.ERIServers.Add(new ()
{
ServerName = string.Format(T("ERI Server {0}"), DateTimeOffset.UtcNow),
});
await this.SettingsManager.StoreSettings();
}
private async Task RemoveERIServer()
{
if (this.IsProcessing)
return;
if(this.selectedERIServer is null)
return;
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, string.Format(T("Are you sure you want to delete the ERI server preset '{0}'?"), this.selectedERIServer.ServerName) },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete ERI server preset"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.SettingsManager.ConfigurationData.ERI.ERIServers.Remove(this.selectedERIServer);
this.selectedERIServer = null;
this.ResetForm();
await this.SettingsManager.StoreSettings();
if (this.Form is not null)
await this.Form.ResetValidationAsync();
}
private bool IsNoneERIServerSelected => this.selectedERIServer is null;
/// <summary>
/// Gets whether ERI configuration input controls should be disabled.
/// </summary>
private bool IsERIInputDisabled => this.IsNoneERIServerSelected || this.IsProcessing;
/// <summary>
/// Gets whether the selected ERI specification cannot be saved.
/// </summary>
private bool IsSpecificationSavingDisabled => !this.selectedERIVersion.WasSpecificationSelected || this.IsERIInputDisabled;
/// <summary>
/// Lets the user save the selected ERI specification, a copy of the one the app carries.
/// </summary>
/// <remarks>
/// The app server answers the app window only, so a link to the file would fail in the browser
/// of the system. The copy also matches the installed version of AI Studio.
/// </remarks>
private async Task SaveSpecificationAsync()
{
var specificationPath = this.selectedERIVersion.SpecificationURL;
var response = await this.RustService.SaveFile(T("Save the ERI specification"), [FileTypes.JSON], Path.GetFileName(specificationPath));
if (response.UserCancelled)
return;
await using var feedback = new FileExportFeedback();
feedback.ShowRunning(response.SaveFilePath);
try
{
if (await this.WebRootFiles.CopyToAsync(specificationPath, response.SaveFilePath))
{
await feedback.SucceededAsync(Icons.Material.Filled.CheckCircle, T("The ERI specification was saved."), response.SaveFilePath, true);
return;
}
this.Logger.LogError("The app carries no ERI specification at '{SpecificationPath}'.", specificationPath);
}
catch (Exception exception)
{
this.Logger.LogError(exception, "Failed to save the ERI specification {ERIVersion}.", this.selectedERIVersion);
}
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, T("The ERI specification could not be saved.")));
}
/// <summary>
/// Gets whether the generated-code target directory selection should be disabled.
/// </summary>
private bool IsBaseDirectorySelectionDisabled => this.IsERIInputDisabled || !this.writeToFilesystem;
/// <summary>
/// Gets a stable row snapshot for the embedding-method table.
/// </summary>
private EmbeddingInfo[] EmbeddingRows => this.embeddings.ToArray();
/// <summary>
/// Gets a stable row snapshot for the retrieval-process table.
/// </summary>
private RetrievalInfo[] RetrievalProcessRows => this.retrievalProcesses.ToArray();
/// <summary>
/// Gets called when the server name was changed by typing.
/// </summary>
/// <remarks>
/// This method is used to update the server name in the selected ERI server preset.
/// Otherwise, the users would be confused when they change the server name and the changes are not reflected in the UI.
/// </remarks>
private void ServerNameWasChanged()
{
if(this.selectedERIServer is null)
return;
this.selectedERIServer.ServerName = this.serverName;
}
private string? ValidateServerName(string name)
{
if(string.IsNullOrWhiteSpace(name))
return T("Please provide a name for your ERI server. This name will be used to identify the server in AI Studio.");
if(name.Length is > 60 or < 6)
return T("The name of your ERI server must be between 6 and 60 characters long.");
if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Where(n => n != this.selectedERIServer).Any(n => n.ServerName == name))
return T("An ERI server preset with this name already exists. Please choose a different name.");
return null;
}
private string? ValidateServerDescription(string description)
{
if(string.IsNullOrWhiteSpace(description))
return T("Please provide a description for your ERI server. What data will the server retrieve? This description will be used to inform users about the purpose of your ERI server.");
if(description.Length is < 32 or > 512)
return T("The description of your ERI server must be between 32 and 512 characters long.");
return null;
}
private string? ValidateERIVersion(ERIVersion version)
{
if (version == ERIVersion.NONE)
return T("Please select an ERI specification version for the ERI server.");
return null;
}
private string? ValidateProgrammingLanguage(ProgrammingLanguages language)
{
if (language == ProgrammingLanguages.OTHER)
return null;
if (language == ProgrammingLanguages.NONE)
return T("Please select a programming language for the ERI server.");
return null;
}
private string? ValidateOtherLanguage(string language)
{
if(this.selectedProgrammingLanguage != ProgrammingLanguages.OTHER)
return null;
if(string.IsNullOrWhiteSpace(language))
return T("Please specify the custom programming language for the ERI server.");
return null;
}
private string? ValidateDataSource(DataSources dataSource)
{
if (dataSource == DataSources.CUSTOM)
return null;
if (dataSource == DataSources.NONE)
return T("Please select a data source for the ERI server.");
return null;
}
private string? ValidateDataSourceProductName(string productName)
{
if(this.selectedDataSource is DataSources.CUSTOM or DataSources.NONE or DataSources.FILE_SYSTEM)
return null;
if(string.IsNullOrWhiteSpace(productName))
return T("Please specify the product name of the data source, e.g., 'MongoDB', 'Redis', 'PostgreSQL', 'Neo4j', or 'MinIO', etc.");
return null;
}
private string? ValidateOtherDataSource(string dataSource)
{
if(this.selectedDataSource != DataSources.CUSTOM)
return null;
if(string.IsNullOrWhiteSpace(dataSource))
return T("Please describe the data source of your ERI server.");
return null;
}
private string? ValidateHostname(string hostname)
{
if(!this.NeedHostnamePort())
return null;
// When using a custom data source, the hostname is optional:
if(this.selectedDataSource is DataSources.CUSTOM)
return null;
if(string.IsNullOrWhiteSpace(hostname))
return T("Please provide the hostname of the data source. Use 'localhost' if the data source is on the same machine as the ERI server.");
if(hostname.Length > 255)
return T("The hostname of the data source must not exceed 255 characters.");
return null;
}
private string? ValidatePort(int? port)
{
if(!this.NeedHostnamePort())
return null;
// When using a custom data source, the port is optional:
if(this.selectedDataSource is DataSources.CUSTOM)
return null;
if(port is null)
return T("Please provide the port of the data source.");
if(port is < 1 or > 65535)
return T("The port of the data source must be between 1 and 65535.");
return null;
}
private void DataSourcePortWasTyped()
{
this.userTypedPort = true;
}
private void DataSourceWasChanged()
{
if(this.selectedERIServer is null)
return;
if (this.selectedDataSource is DataSources.NONE)
{
this.selectedERIServer.DataSourcePort = null;
this.dataSourcePort = null;
this.userTypedPort = false;
return;
}
if(this.userTypedPort)
return;
//
// Preselect the default port for the selected data source
//
this.dataSourcePort = this.selectedDataSource switch
{
DataSources.DOCUMENT_STORE => 27017,
DataSources.KEY_VALUE_STORE => 6379,
DataSources.OBJECT_STORAGE => 9000,
DataSources.RELATIONAL_DATABASE => 5432,
DataSources.GRAPH_DATABASE => 7687,
_ => null
};
}
private string? ValidateAuthenticationMethods(Auth _)
{
var authenticationMethods = (this.selectedAuthenticationMethods as HashSet<Auth>)!;
if(authenticationMethods.Count == 0)
return T("Please select at least one authentication method for the ERI server.");
return null;
}
private void AuthenticationMethodWasChanged(IEnumerable<Auth>? selectedValues)
{
if(selectedValues is null)
{
this.selectedAuthenticationMethods = [];
this.selectedOperatingSystem = OperatingSystem.NONE;
return;
}
this.selectedAuthenticationMethods = selectedValues;
if(!this.IsUsingKerberos())
this.selectedOperatingSystem = OperatingSystem.NONE;
}
private bool IsUsingKerberos()
{
return this.selectedAuthenticationMethods.Contains(Auth.KERBEROS);
}
private string? ValidateOperatingSystem(OperatingSystem os)
{
if(!this.IsUsingKerberos())
return null;
if(os is OperatingSystem.NONE)
return T("Please select the operating system on which the ERI server will run. This is necessary when using SSO with Kerberos.");
return null;
}
private string? ValidateAllowedLLMProviders(AllowedLLMProviders provider)
{
if(provider == AllowedLLMProviders.NONE)
return T("Please select which types of LLMs users are allowed to use with the data from this ERI server.");
return null;
}
private string AuthDescriptionTitle() => this.IsAuthDescriptionOptional() ? T("(Optional) Describe how you planned the authentication process") : T("Describe how you planned the authentication process");
private bool IsAuthDescriptionOptional()
{
if (this.selectedAuthenticationMethods is not HashSet<Auth> authenticationMethods)
return true;
if(authenticationMethods.Count > 1)
return false;
if (authenticationMethods.Any(n => n == Auth.NONE) && authenticationMethods.Count > 1)
return false;
return true;
}
private string? ValidateAuthDescription(string description)
{
var authenticationMethods = (this.selectedAuthenticationMethods as HashSet<Auth>)!;
if(authenticationMethods.Any(n => n == Auth.NONE) && authenticationMethods.Count > 1 && string.IsNullOrWhiteSpace(this.authDescription))
return T("Please describe how the selected authentication methods should be used. Especially, explain for what data the NONE method (public access) is used.");
if(authenticationMethods.Count > 1 && string.IsNullOrWhiteSpace(this.authDescription))
return T("Please describe how the selected authentication methods should be used.");
return null;
}
private string? ValidateDirectory(string path)
{
if(!this.writeToFilesystem)
return null;
if(string.IsNullOrWhiteSpace(path))
return T("Please provide a base directory for the ERI server to write files to.");
return null;
}
private string GetMultiSelectionAuthText(List<Auth> selectedValues)
{
if(selectedValues.Count == 0)
return T("Please select at least one authentication method");
if(selectedValues.Count == 1)
return T("You have selected 1 authentication method");
return string.Format(T("You have selected {0} authentication methods"), selectedValues.Count);
}
private bool NeedHostnamePort()
{
switch (this.selectedDataSource)
{
case DataSources.NONE:
case DataSources.FILE_SYSTEM:
return false;
default:
return true;
}
}
private async Task AddEmbedding()
{
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<EmbeddingMethodDialog>
{
{ x => x.IsEditing, false },
{ x => x.UsedEmbeddingMethodNames, this.embeddings.Select(n => n.EmbeddingName).ToList() },
};
var dialogReference = await this.DialogService.ShowAsync<EmbeddingMethodDialog>(T("Add Embedding Method"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var addedEmbedding = (EmbeddingInfo)dialogResult.Data!;
this.embeddings.Add(addedEmbedding);
await this.AutoSave();
}
private async Task EditEmbedding(EmbeddingInfo embeddingInfo)
{
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<EmbeddingMethodDialog>
{
{ x => x.DataEmbeddingName, embeddingInfo.EmbeddingName },
{ x => x.DataEmbeddingType, embeddingInfo.EmbeddingType },
{ x => x.DataDescription, embeddingInfo.Description },
{ x => x.DataUsedWhen, embeddingInfo.UsedWhen },
{ x => x.DataLink, embeddingInfo.Link },
{ x => x.UsedEmbeddingMethodNames, this.embeddings.Where(n => n != embeddingInfo).Select(n => n.EmbeddingName).ToList() },
{ x => x.IsEditing, true },
};
var dialogReference = await this.DialogService.ShowAsync<EmbeddingMethodDialog>(T("Edit Embedding Method"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var editedEmbedding = (EmbeddingInfo)dialogResult.Data!;
this.embeddings[this.embeddings.IndexOf(embeddingInfo)] = editedEmbedding;
await this.AutoSave();
}
private async Task DeleteEmbedding(EmbeddingInfo embeddingInfo)
{
if (this.IsProcessing)
return;
var message = this.retrievalProcesses.Any(n => n.Embeddings?.Contains(embeddingInfo) is true)
? string.Format(T("The embedding '{0}' is used in one or more retrieval processes. Are you sure you want to delete it?"), embeddingInfo.EmbeddingName)
: string.Format(T("Are you sure you want to delete the embedding '{0}'?"), embeddingInfo.EmbeddingName);
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, message },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Embedding"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.retrievalProcesses.ForEach(n => n.Embeddings?.Remove(embeddingInfo));
this.embeddings.Remove(embeddingInfo);
await this.AutoSave();
}
private async Task AddRetrievalProcess()
{
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<RetrievalProcessDialog>
{
{ x => x.IsEditing, false },
{ x => x.AvailableEmbeddings, this.embeddings },
{ x => x.UsedRetrievalProcessNames, this.retrievalProcesses.Select(n => n.Name).ToList() },
};
var dialogReference = await this.DialogService.ShowAsync<RetrievalProcessDialog>(T("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)
{
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<RetrievalProcessDialog>
{
{ 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 },
{ x => x.UsedRetrievalProcessNames, this.retrievalProcesses.Where(n => n != retrievalInfo).Select(n => n.Name).ToList() },
};
var dialogReference = await this.DialogService.ShowAsync<RetrievalProcessDialog>(T("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)
{
if (this.IsProcessing)
return;
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Message, string.Format(T("Are you sure you want to delete the retrieval process '{0}'?"), retrievalInfo.Name) },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("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()
{
if(this.IsNoneERIServerSelected)
return;
await this.AutoSave();
await this.Form!.ValidateAsync();
if (!this.InputIsValid)
return;
if(this.retrievalProcesses.Count == 0)
{
this.AddInputIssue(T("Please describe at least one retrieval process."));
return;
}
var writeToFilesystemSnapshot = this.writeToFilesystem;
var baseDirectorySnapshot = this.baseDirectory;
var previouslyGeneratedFilesSnapshot = this.previouslyGeneratedFiles.ToArray();
this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.WebRootFiles);
if (string.IsNullOrWhiteSpace(this.eriSpecification))
{
this.AddInputIssue(T("The ERI specification could not be loaded. Please try again later."));
return;
}
var now = DateTimeOffset.UtcNow;
this.CreateChatThread(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, $"{now:yyyy-MM-dd HH:mm} - {this.serverName}");
//
// ---------------------------------
// Ask for files (viewable in the chat)
// ---------------------------------
//
var time = this.AddUserRequest("""
Please list all the files you want to create. Provide the result as a Markdown list.
Start with a brief message that these are the files we are now creating.
""", true);
await this.AddAIResponseAsync(time);
//
// ---------------------------------
// Ask for files, again (JSON output, invisible)
// ---------------------------------
//
time = this.AddUserRequest("""
Please format all the files you want to create as a JSON object, without Markdown.
Use the following JSON schema:
{
"files": [
"path/to/file1",
"path/to/file2"
]
}
""", true);
var fileListAnswer = await this.AddAIResponseAsync(time, true);
if (!ERIServerFileList.TryParse(fileListAnswer, out var filesToGenerate))
{
this.Logger.LogWarning("The list of files could not be read from the answer of the model: '{Answer}'", fileListAnswer);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.SaveAs, T("The list of files in the answer of the model could not be read, so no files were created. Please try again.")));
return;
}
//
// Is this an update of the ERI server? If so, we need to delete the previously generated files.
// Only those within the current base directory, though: the list names files by their full path,
// and it outlives a change of the base directory, as well as a restore on another computer.
// There, the same path may belong to files of somebody else:
//
if (writeToFilesystemSnapshot && previouslyGeneratedFilesSnapshot.Length > 0)
{
foreach (var file in previouslyGeneratedFilesSnapshot)
{
if (!PathTools.IsInsideDirectory(baseDirectorySnapshot, file))
{
this.Logger.LogWarning($"The previously created file '{file}' is not inside the base directory '{baseDirectorySnapshot}' and was kept.");
continue;
}
try
{
if (File.Exists(file))
{
File.Delete(file);
this.Logger.LogInformation($"The previously created file '{file}' was deleted.");
}
else
{
this.Logger.LogWarning($"The previously created file '{file}' could not be found.");
}
}
catch (Exception e)
{
this.Logger.LogWarning($"The previously created file '{file}' could not be deleted: {e.Message}");
}
}
}
var generatedFiles = new List<string>();
foreach (var file in filesToGenerate)
{
this.Logger.LogInformation($"The LLM want to create the file: '{file}'");
//
// ---------------------------------
// Ask the AI to create another file
// ---------------------------------
//
time = this.AddUserRequest($"""
Please create the file `{file}`. Your output is formatted in Markdown
using the following template:
## file/path
```language
content of the file
```
When the file itself contains code blocks, enclose its content in a longer
fence, such as four backticks.
""", true);
var generatedCodeMarkdown = await this.AddAIResponseAsync(time);
if (writeToFilesystemSnapshot)
{
var outcome = await ERIServerFiles.WriteAsync(baseDirectorySnapshot, file, generatedCodeMarkdown, this.Logger);
if (outcome is ERIServerFileOutcome.WRITTEN)
generatedFiles.Add(ERIServerFiles.PathOf(baseDirectorySnapshot, file));
else
await this.MessageBus.SendWarning(new(Icons.Material.Filled.SaveAs, outcome.ToUserMessage(file)));
}
}
if(writeToFilesystemSnapshot)
{
this.previouslyGeneratedFiles = generatedFiles;
this.selectedERIServer!.PreviouslyGeneratedFiles = generatedFiles;
await this.SettingsManager.StoreSettings();
}
//
// ---------------------------------
// Ask the AI for further steps
// ---------------------------------
//
time = this.AddUserRequest("""
Thank you for implementing the files. Please explain what the next steps are.
The goal is for the code to compile and the server to start. We assume that
the developer has installed the compiler. We will not consider DevOps tools
like Docker.
""", true);
await this.AddAIResponseAsync(time);
}
}