diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 0c4509e8d..afbc6ea57 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2164,6 +2164,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence." +-- Embedding Result +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result" -- Generate an encryption secret and copy it to the clipboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"] = "Generate an encryption secret and copy it to the clipboard" @@ -2173,6 +2175,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete" +-- Test Embedding Provider +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider" + -- Add Embedding UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding" @@ -2185,6 +2190,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" +-- Embedding Vector (one dimension per line) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)" + -- Model UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model" @@ -2194,6 +2202,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199 -- Name UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" +-- No embedding was returned. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned." + -- Configured Embedding Providers UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers" @@ -2203,6 +2214,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512 -- Edit UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit" +-- Close +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close" + -- Actions UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions" @@ -2224,6 +2238,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard" +-- Test +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test" + -- Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" @@ -3328,6 +3345,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel" +-- Embedding Vector +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index e68fdeee9..9d14a99aa 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -62,6 +62,9 @@ + + + } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index 0f78bb979..4ff922fdc 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -1,4 +1,6 @@ +using System.Globalization; using AIStudio.Dialogs; +using AIStudio.Provider; using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -134,4 +136,48 @@ private async Task UpdateEmbeddingProviders() await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders); } + + private async Task TestEmbeddingProvider(EmbeddingProvider provider) + { + var dialogParameters = new DialogParameters + { + { x => x.ConfirmText, "Embed text" }, + { x => x.InputHeaderText, "Add text that should be embedded:" }, + { x => x.UserInput, "Add text here"} + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Test Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var inputText = dialogResult.Data as string; + if (string.IsNullOrWhiteSpace(inputText)) + return; + + var embeddingProvider = provider.CreateProvider(); + var embeddings = await embeddingProvider.EmbedTextAsync(provider.Model, this.SettingsManager, default, new List { inputText }); + + if (embeddings.Count == 0) + { + await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close")); + return; + } + + var vector = embeddings.FirstOrDefault(); + if (vector is null || vector.Count == 0) + { + await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close")); + return; + } + + var resultText = string.Join(Environment.NewLine, vector.Select(value => value.ToString("G9", CultureInfo.InvariantCulture))); + var resultParameters = new DialogParameters + { + { x => x.ResultText, resultText }, + { x => x.ResultLabel, T("Embedding Vector (one dimension per line)") }, + }; + + await this.DialogService.ShowAsync(T("Embedding Result"), resultParameters, DialogOptions.FULLSCREEN); + } } diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor new file mode 100644 index 000000000..8e1408ef3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor @@ -0,0 +1,22 @@ +@inherits MSGComponentBase + + + + + + + + @T("Close") + + + diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs new file mode 100644 index 000000000..96830edf3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs @@ -0,0 +1,21 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class EmbeddingResultDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public string ResultText { get; set; } = string.Empty; + + [Parameter] + public string ResultLabel { get; set; } = string.Empty; + + private string ResultLabelText => string.IsNullOrWhiteSpace(this.ResultLabel) ? T("Embedding Vector") : this.ResultLabel; + + private void Close() => this.MudDialog.Close(); +} diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index d95f1a6a9..88bb1cba6 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -2166,6 +2166,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Möchten Sie einen Anbieter als Standard für die gesamte App festlegen? Wenn Sie einen anderen Anbieter für einen Assistenten konfigurieren, hat dieser immer Vorrang." +-- Embedding Result +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Einbettungsergebnis" -- Generate an encryption secret and copy it to the clipboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"] = "Geheimnis für die Verschlüsselung generieren und in die Zwischenablage kopieren" @@ -2175,6 +2177,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Löschen" +-- Test Embedding Provider +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Embedding-Anbieter testen" + -- Add Embedding UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Einbettung hinzufügen" @@ -2187,6 +2192,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen" +-- Embedding Vector (one dimension per line) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Einbettungsvektor (eine Dimension pro Zeile)" + -- Model UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Modell" @@ -2196,6 +2204,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199 -- Name UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" +-- No embedding was returned. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "Es wurde keine Einbettung zurückgegeben." + -- Configured Embedding Providers UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Konfigurierte Anbieter für Einbettungen" @@ -2205,6 +2216,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512 -- Edit UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Bearbeiten" +-- Close +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Schließen" + -- Actions UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen" @@ -2226,6 +2240,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Dashboard öffnen" +-- Test +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Testen" + -- Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter" @@ -3330,6 +3347,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Anb -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Abbrechen" +-- Embedding Vector +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einbettungsvektor" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 688cb8d0a..58b6e2a67 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -2166,6 +2166,8 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence." +-- Embedding Result +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result" -- Generate an encryption secret and copy it to the clipboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"] = "Generate an encryption secret and copy it to the clipboard" @@ -2175,6 +2177,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete" +-- Test Embedding Provider +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider" + -- Add Embedding UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding" @@ -2187,6 +2192,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" +-- Embedding Vector (one dimension per line) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)" + -- Model UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model" @@ -2196,6 +2204,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199 -- Name UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" +-- No embedding was returned. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned." + -- Configured Embedding Providers UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers" @@ -2205,6 +2216,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512 -- Edit UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit" +-- Close +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close" + -- Actions UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions" @@ -2226,6 +2240,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard" +-- Test +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test" + -- Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" @@ -3330,6 +3347,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel" +-- Embedding Vector +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index de46e95b1..d39351b2f 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -86,6 +86,13 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index b536ee4db..533670ad7 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -113,6 +113,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 0cf8a362e..49ac2feed 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -98,6 +99,9 @@ protected BaseProvider(LLMProviders provider, string url, ILogger logger) /// public abstract Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default); + /// + public abstract Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts); + /// public abstract Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default); @@ -645,6 +649,82 @@ protected async Task PerformStandardTranscriptionRequest(RequestedSecret } } + protected async Task>> PerformStandardTextEmbeddingRequest(RequestedSecret requestedSecret, Model embeddingModel, Host host = Host.NONE, CancellationToken token = default, params List texts) + { + try + { + // + // Add the model name to the form data. Ensure that a model name is always provided. + // Otherwise, the StringContent constructor will throw an exception. + // + var modelName = embeddingModel.Id; + if (string.IsNullOrWhiteSpace(modelName)) + modelName = "placeholder"; + + // Prepare the HTTP embedding request: + var payload = new + { + model = modelName, + input = texts, + encoding_format = "float" + }; + var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); + + using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL()); + + // Handle the authorization header based on the provider: + switch (this.Provider) + { + case LLMProviders.SELF_HOSTED: + if(requestedSecret.Success) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + break; + + default: + if(!requestedSecret.Success) + { + this.logger.LogError("No valid API key available for embedding request."); + return Array.Empty>(); + } + + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + break; + } + + // Set the content: + request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json"); + + using var response = await this.httpClient.SendAsync(request, token); + var responseBody = response.Content.ReadAsStringAsync(token).Result; + + if (!response.IsSuccessStatusCode) + { + this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); + return Array.Empty>(); + } + + var embeddingResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS); + if (embeddingResponse is { Data: not null }) + { + return embeddingResponse.Data + .Select(d => d.Embedding?.ToArray() ?? Array.Empty()) + .Cast>() + .ToArray(); + } + else + { + this.logger.LogError("Was not able to deserialize the embedding response."); + return Array.Empty>(); + } + } + catch (Exception e) + { + this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message); + return Array.Empty>(); + } + } + /// /// Parse and convert API parameters from a provided JSON string into a dictionary, /// optionally merging additional parameters and removing specific keys. diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index ce33f2886..ce86ef68b 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -86,6 +86,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/EmbeddingResponse.cs b/app/MindWork AI Studio/Provider/EmbeddingResponse.cs new file mode 100644 index 000000000..89cf5fb8d --- /dev/null +++ b/app/MindWork AI Studio/Provider/EmbeddingResponse.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Provider; + +public sealed record EmbeddingResponse +{ + public string? Id { get; set; } + public string? Object { get; set; } + public List? Data { get; set; } + public string? Model { get; set; } + public Usage? Usage { get; set; } +} + +public sealed record EmbeddingData +{ + public string? Object { get; set; } + public List? Embedding { get; set; } + public int? Index { get; set; } +} + +public sealed record Usage +{ + public int? PromptTokens { get; set; } + public int? TotalTokens { get; set; } + public int? CompletionTokens { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 1eb21894d..cd7a5abd7 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -88,6 +88,12 @@ public override async Task TranscribeAudioAsync(Model transcriptionModel var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 2b7e4dcb0..784faa457 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -87,6 +87,12 @@ public override async Task TranscribeAudioAsync(Model transcriptionModel var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index 48dea49e9..224fe5e6c 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -87,6 +87,78 @@ public override Task TranscribeAudioAsync(Provider.Model transcriptionMo { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + try + { + var modelName = embeddingModel.Id; + if (string.IsNullOrWhiteSpace(modelName)) + { + LOGGER.LogError("No model name provided for embedding request."); + return Array.Empty>(); + } + + if (modelName.StartsWith("models/", StringComparison.OrdinalIgnoreCase)) + modelName = modelName.Substring("models/".Length); + + if (!requestedSecret.Success) + { + LOGGER.LogError("No valid API key available for embedding request."); + return Array.Empty>(); + } + + // Prepare the Google Gemini embedding request: + var payload = new + { + content = new + { + parts = texts.Select(text => new { text }).ToArray() + }, + taskType = "SEMANTIC_SIMILARITY" + }; + var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); + + var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent"; + using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl); + request.Headers.Add("x-goog-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json"); + + using var response = await this.httpClient.SendAsync(request, token); + var responseBody = await response.Content.ReadAsStringAsync(token); + + if (!response.IsSuccessStatusCode) + { + LOGGER.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); + return Array.Empty>(); + } + + var embeddingResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS); + + if (embeddingResponse is { Embedding: not null }) + { + return embeddingResponse.Embedding + .Select(d => d.Values?.ToArray() ?? Array.Empty()) + .Cast>() + .ToArray(); + } + else + { + LOGGER.LogError("Was not able to deserialize the embedding response."); + return Array.Empty>(); + } + + } + catch (Exception e) + { + LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message); + return Array.Empty>(); + } + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) @@ -150,4 +222,14 @@ private async Task LoadModels(SecretStoreType storeType, Cancell var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse; } -} \ No newline at end of file + + private sealed record GoogleEmbeddingResponse + { + public List? Embedding { get; set; } + } + + private sealed record GoogleEmbedding + { + public List? Values { get; set; } + } +} diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 07cdb3900..1802150b9 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -87,6 +87,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index ec5fca2c3..3df909c08 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -86,6 +86,13 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index a05ca11e2..47db58e8e 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -91,6 +91,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index 5c3900746..ef15dd21b 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -59,6 +59,16 @@ public interface IProvider /// The cancellation token. /// >The transcription result. public Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default); + + /// + /// Embed a text file. + /// + /// The model to use for embedding. + /// The settings manager instance to use. + /// The cancellation token. + /// /// A single string or a list of strings to embed. + /// >The embedded text as a single vector or as a list of vectors. + public Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts); /// /// Load all possible text models that can be used with this provider. diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 6685e6d6e..f4cb07f4a 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -89,6 +89,13 @@ public override async Task TranscribeAudioAsync(Provider.Model transcrip return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } + /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) { diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index a650ac343..cb24e0faa 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -40,6 +40,8 @@ public async IAsyncEnumerable StreamImageCompletion(Model imageModel, public Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty); + public Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) => Task.FromResult>>(Array.Empty>()); + public IReadOnlyCollection GetModelCapabilities(Model model) => [ Capability.NONE ]; #endregion diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index d2d0b32b5..90194ed77 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -224,6 +224,13 @@ public override async Task TranscribeAudioAsync(Model transcriptionModel var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index ca8ef1554..444703c5a 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -94,6 +94,13 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index 691dcdd54..804c8334a 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -94,6 +94,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs index 6c475273e..25dc07ca1 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs @@ -30,6 +30,11 @@ public static class HostExtensions _ => "audio/transcriptions", }; + public static string EmbeddingURL(this Host host) => host switch + { + _ => "embeddings", + }; + public static bool IsChatSupported(this Host host) { switch (host) diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index a1e411e18..9b3d6d674 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -95,6 +95,13 @@ public override async Task TranscribeAudioAsync(Provider.Model transcrip return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token); } + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } + public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) { try diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index a0510dd6a..22abaef02 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -88,6 +88,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri return Task.FromResult(string.Empty); } + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } + /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) {