using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.Validation; using Microsoft.AspNetCore.Components; using Host = AIStudio.Provider.SelfHosted.Host; namespace AIStudio.Dialogs; public partial class EmbeddingProviderDialog : ComponentBase, ISecretId { [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!; /// /// The embedding's number in the list. /// [Parameter] public uint DataNum { get; set; } /// /// The embedding's ID. /// [Parameter] public string DataId { get; set; } = Guid.NewGuid().ToString(); /// /// The user chosen name. /// [Parameter] public string DataName { get; set; } = string.Empty; /// /// The chosen hostname for self-hosted providers. /// [Parameter] public string DataHostname { get; set; } = string.Empty; /// /// The host to use, e.g., llama.cpp. /// [Parameter] public Host DataHost { get; set; } = Host.NONE; /// /// Is this provider self-hosted? /// [Parameter] public bool IsSelfHosted { get; set; } /// /// The provider to use. /// [Parameter] public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE; /// /// The embedding model to use. /// [Parameter] public Model DataModel { get; set; } /// /// Should the dialog be in editing mode? /// [Parameter] public bool IsEditing { get; init; } [Inject] private SettingsManager SettingsManager { get; init; } = null!; [Inject] private ILogger Logger { get; init; } = null!; [Inject] private RustService RustService { get; init; } = null!; private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); /// /// The list of used instance names. We need this to check for uniqueness. /// private List UsedInstanceNames { get; set; } = []; private bool dataIsValid; private string[] dataIssues = []; private string dataAPIKey = string.Empty; private string dataManuallyModel = string.Empty; private string dataAPIKeyStorageIssue = string.Empty; private string dataEditingPreviousInstanceName = string.Empty; // We get the form reference from Blazor code to validate it manually: private MudForm form = null!; private readonly List availableModels = new(); private readonly Encryption encryption = Program.ENCRYPTION; private readonly ProviderValidation providerValidation; public EmbeddingProviderDialog() { this.providerValidation = new() { GetProvider = () => this.DataLLMProvider, GetAPIKeyStorageIssue = () => this.dataAPIKeyStorageIssue, GetPreviousInstanceName = () => this.dataEditingPreviousInstanceName, GetUsedInstanceNames = () => this.UsedInstanceNames, GetHost = () => this.DataHost, }; } private EmbeddingProvider CreateEmbeddingProviderSettings() { var cleanedHostname = this.DataHostname.Trim(); return new() { Num = this.DataNum, Id = this.DataId, Name = this.DataName, UsedLLMProvider = this.DataLLMProvider, Model = this.DataLLMProvider is LLMProviders.SELF_HOSTED ? new Model(this.dataManuallyModel, null) : this.DataModel, IsSelfHosted = this.DataLLMProvider is LLMProviders.SELF_HOSTED, Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, Host = this.DataHost, }; } #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.UsedInstanceNames = this.SettingsManager.ConfigurationData.EmbeddingProviders.Select(x => x.Name.ToLowerInvariant()).ToList(); // When editing, we need to load the data: if(this.IsEditing) { this.dataEditingPreviousInstanceName = this.DataName.ToLowerInvariant(); // When using self-hosted embedding, we must copy the model name: if (this.DataLLMProvider is LLMProviders.SELF_HOSTED) this.dataManuallyModel = this.DataModel.Id; // // We cannot load the API key for self-hosted providers: // if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && this.DataHost is not Host.OLLAMA) { await this.ReloadModels(); await base.OnInitializedAsync(); return; } // Load the API key: var requestedSecret = await this.RustService.GetAPIKey(this, isTrying: this.DataLLMProvider is LLMProviders.SELF_HOSTED); if (requestedSecret.Success) this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.encryption); else { this.dataAPIKey = string.Empty; if (this.DataLLMProvider is not LLMProviders.SELF_HOSTED) { this.dataAPIKeyStorageIssue = $"Failed to load the API key from the operating system. The message was: {requestedSecret.Issue}. You might ignore this message and provide the API key again."; await this.form.Validate(); } } await this.ReloadModels(); } 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 #region Implementation of ISecretId public string SecretId => this.DataId; public string SecretName => this.DataName; #endregion private async Task Store() { await this.form.Validate(); if (!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue)) this.dataAPIKeyStorageIssue = string.Empty; // When the data is not valid, we don't store it: if (!this.dataIsValid) return; // Use the data model to store the provider. // We just return this data to the parent component: var addedProviderSettings = this.CreateEmbeddingProviderSettings(); if (!string.IsNullOrWhiteSpace(this.dataAPIKey)) { // Store the API key in the OS secure storage: var storeResponse = await this.RustService.SetAPIKey(this, this.dataAPIKey); if (!storeResponse.Success) { this.dataAPIKeyStorageIssue = $"Failed to store the API key in the operating system. The message was: {storeResponse.Issue}. Please try again."; await this.form.Validate(); return; } } this.MudDialog.Close(DialogResult.Ok(addedProviderSettings)); } private string? ValidateManuallyModel(string manuallyModel) { if (this.DataLLMProvider is LLMProviders.SELF_HOSTED && string.IsNullOrWhiteSpace(manuallyModel)) return "Please enter an embedding model name."; return null; } private void Cancel() => this.MudDialog.Cancel(); private async Task ReloadModels() { var currentEmbeddingProviderSettings = this.CreateEmbeddingProviderSettings(); var provider = currentEmbeddingProviderSettings.CreateProvider(this.Logger); if(provider is NoProvider) return; var models = await provider.GetEmbeddingModels(this.dataAPIKey); // Order descending by ID means that the newest models probably come first: var orderedModels = models.OrderByDescending(n => n.Id); this.availableModels.Clear(); this.availableModels.AddRange(orderedModels); } private string APIKeyText => this.DataLLMProvider switch { LLMProviders.SELF_HOSTED => "(Optional) API Key", _ => "API Key", }; private bool IsNoneProvider => this.DataLLMProvider is LLMProviders.NONE; }