From 8cbef49d89fdfd9e73a39b46ab9acf54ec3ba628 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 1 Jan 2025 15:49:27 +0100 Subject: [PATCH] Add ERI server assistant (#231) --- README.md | 1 + app/MindWork AI Studio.sln.DotSettings | 2 + .../Agenda/AssistantAgenda.razor.cs | 2 +- .../Assistants/AssistantBase.razor | 15 +- .../Assistants/AssistantBase.razor.cs | 52 +- .../BiasDay/BiasOfTheDayAssistant.razor.cs | 8 +- .../Coding/AssistantCoding.razor.cs | 2 +- .../Assistants/Coding/CodingContextItem.razor | 2 +- .../Assistants/EMail/AssistantEMail.razor.cs | 2 +- .../Assistants/ERI/AllowedLLMProviders.cs | 9 + .../ERI/AllowedLLMProvidersExtensions.cs | 16 + .../Assistants/ERI/AssistantERI.razor | 349 ++++++ .../Assistants/ERI/AssistantERI.razor.cs | 1087 +++++++++++++++++ app/MindWork AI Studio/Assistants/ERI/Auth.cs | 9 + .../Assistants/ERI/AuthExtensions.cs | 26 + .../Assistants/ERI/DataSources.cs | 15 + .../Assistants/ERI/DataSourcesExtensions.cs | 19 + .../Assistants/ERI/ERIVersion.cs | 8 + .../Assistants/ERI/ERIVersionExtensions.cs | 27 + .../Assistants/ERI/EmbeddingInfo.cs | 19 + .../Assistants/ERI/OperatingSystem.cs | 9 + .../ERI/OperatingSystemExtensions.cs | 14 + .../Assistants/ERI/ProgrammingLanguages.cs | 20 + .../ERI/ProgrammingLanguagesExtensions.cs | 24 + .../Assistants/ERI/RetrievalInfo.cs | 18 + .../Assistants/ERI/RetrievalParameter.cs | 14 + .../AssistantGrammarSpelling.razor.cs | 2 +- .../IconFinder/AssistantIconFinder.razor.cs | 2 +- .../JobPosting/AssistantJobPostings.razor.cs | 2 +- .../LegalCheck/AssistantLegalCheck.razor.cs | 2 +- .../MyTasks/AssistantMyTasks.razor.cs | 2 +- .../AssistantRewriteImprove.razor.cs | 2 +- .../Synonym/AssistantSynonyms.razor.cs | 2 +- .../AssistantTextSummarizer.razor.cs | 2 +- .../Translation/AssistantTranslation.razor.cs | 2 +- .../Chat/KnownWorkspaces.cs | 7 + .../Components/EnumSelection.razor | 2 +- .../Components/SelectDirectory.razor | 16 + .../Components/SelectDirectory.razor.cs | 60 + .../Components/Workspaces.razor.cs | 16 - .../Dialogs/EmbeddingMethodDialog.razor | 109 ++ .../Dialogs/EmbeddingMethodDialog.razor.cs | 144 +++ ...og.razor => EmbeddingProviderDialog.razor} | 0 ...or.cs => EmbeddingProviderDialog.razor.cs} | 4 +- .../Dialogs/RetrievalProcessDialog.razor | 213 ++++ .../Dialogs/RetrievalProcessDialog.razor.cs | 213 ++++ app/MindWork AI Studio/Pages/Assistants.razor | 5 + .../Pages/Assistants.razor.cs | 8 +- app/MindWork AI Studio/Pages/Chat.razor.cs | 9 +- app/MindWork AI Studio/Pages/Settings.razor | 19 +- .../Pages/Settings.razor.cs | 8 +- .../Provider/Anthropic/ProviderAnthropic.cs | 43 +- .../Provider/BaseProvider.cs | 43 + .../Provider/Fireworks/ProviderFireworks.cs | 35 +- .../Provider/Google/ProviderGoogle.cs | 35 +- .../Provider/Groq/ProviderGroq.cs | 35 +- .../Provider/Mistral/ProviderMistral.cs | 35 +- .../Provider/OpenAI/ProviderOpenAI.cs | 35 +- .../Provider/SelfHosted/ProviderSelfHosted.cs | 33 +- app/MindWork AI Studio/Routes.razor.cs | 1 + .../Settings/DataModel/Data.cs | 2 + .../Settings/DataModel/DataERI.cs | 36 + .../Settings/DataModel/DataERIServer.cs | 113 ++ app/MindWork AI Studio/Tools/Components.cs | 1 + .../Tools/ComponentsExtensions.cs | 5 + .../Tools/HttpRateLimitedStreamResult.cs | 23 + .../Tools/Rust/DirectorySelectionResponse.cs | 8 + .../Tools/Rust/PreviousDirectory.cs | 7 + app/MindWork AI Studio/Tools/RustService.cs | 13 + .../Tools/WorkspaceBehaviour.cs | 16 + app/MindWork AI Studio/packages.lock.json | 2 +- .../wwwroot/changelog/v0.9.23.md | 6 + .../wwwroot/specs/eri/v1.json | 531 ++++++++ runtime/Cargo.toml | 2 +- runtime/src/app_window.rs | 53 +- runtime/src/runtime_api.rs | 1 + 76 files changed, 3581 insertions(+), 153 deletions(-) create mode 100644 app/MindWork AI Studio/Assistants/ERI/AllowedLLMProviders.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor create mode 100644 app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/Auth.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/DataSources.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/ERIVersion.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/EmbeddingInfo.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/OperatingSystem.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguages.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/RetrievalInfo.cs create mode 100644 app/MindWork AI Studio/Assistants/ERI/RetrievalParameter.cs create mode 100644 app/MindWork AI Studio/Chat/KnownWorkspaces.cs create mode 100644 app/MindWork AI Studio/Components/SelectDirectory.razor create mode 100644 app/MindWork AI Studio/Components/SelectDirectory.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs rename app/MindWork AI Studio/Dialogs/{EmbeddingDialog.razor => EmbeddingProviderDialog.razor} (100%) rename app/MindWork AI Studio/Dialogs/{EmbeddingDialog.razor.cs => EmbeddingProviderDialog.razor.cs} (98%) create mode 100644 app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataERI.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataERIServer.cs create mode 100644 app/MindWork AI Studio/Tools/HttpRateLimitedStreamResult.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/DirectorySelectionResponse.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/PreviousDirectory.cs create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md create mode 100644 app/MindWork AI Studio/wwwroot/specs/eri/v1.json diff --git a/README.md b/README.md index d280d8c..80d7d5a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Things we are currently working on: - [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~ - [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~ - [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~ + - [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~ - [ ] App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) - [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings index 568505b..c9d147b 100644 --- a/app/MindWork AI Studio.sln.DotSettings +++ b/app/MindWork AI Studio.sln.DotSettings @@ -1,5 +1,7 @@  AI + EDI + ERI LLM LM MSG diff --git a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs index 00f0d26..531c2df 100644 --- a/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs +++ b/app/MindWork AI Studio/Assistants/Agenda/AssistantAgenda.razor.cs @@ -106,7 +106,7 @@ public partial class AssistantAgenda : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputContent = string.Empty; this.contentLines.Clear(); diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 180c449..5387d8e 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -6,7 +6,7 @@ - + @this.Description @@ -35,11 +35,22 @@
- @if (this.ShowResult && this.resultingContentBlock is not null) + @if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null) { } + @if(this.ShowResult && this.ShowEntireChatThread && this.chatThread is not null) + { + foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time)) + { + @if (!block.HideFromUser) + { + + } + } + } +
diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 4bcd672..99f29df 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -4,7 +4,10 @@ using AIStudio.Settings; using Microsoft.AspNetCore.Components; +using MudBlazor.Utilities; + using RustService = AIStudio.Tools.RustService; +using Timer = System.Timers.Timer; namespace AIStudio.Assistants; @@ -55,7 +58,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver _ => string.Empty, }; - protected abstract void ResetFrom(); + protected abstract void ResetForm(); protected abstract bool MightPreselectValues(); @@ -68,6 +71,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver private protected virtual RenderFragment? Body => null; protected virtual bool ShowResult => true; + + protected virtual bool ShowEntireChatThread => false; protected virtual bool AllowProfiles => true; @@ -91,8 +96,10 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver protected MudForm? form; protected bool inputIsValid; protected Profile currentProfile = Profile.NO_PROFILE; - protected ChatThread? chatThread; + + private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6)); + private ContentBlock? resultingContentBlock; private string[] inputIssues = []; private bool isProcessing; @@ -101,6 +108,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver protected override async Task OnInitializedAsync() { + this.formChangeTimer.AutoReset = false; + this.formChangeTimer.Elapsed += async (_, _) => + { + this.formChangeTimer.Stop(); + await this.OnFormChange(); + }; + this.MightPreselectValues(); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); @@ -161,7 +175,35 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver return null; } + + private void TriggerFormChange(FormFieldChangedEventArgs _) + { + this.formChangeTimer.Stop(); + this.formChangeTimer.Start(); + } + /// + /// This method is called after any form field has changed. + /// + /// + /// This method is called after a delay of 1.6 seconds. This is to prevent + /// the method from being called too often. This method is called after + /// the user has stopped typing or selecting options. + /// + protected virtual Task OnFormChange() => Task.CompletedTask; + + /// + /// Add an issue to the UI. + /// + /// The issue to add. + protected void AddInputIssue(string issue) + { + Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1); + this.inputIssues[^1] = issue; + this.inputIsValid = false; + this.StateHasChanged(); + } + protected void CreateChatThread() { this.chatThread = new() @@ -221,7 +263,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver return time; } - protected async Task AddAIResponseAsync(DateTimeOffset time) + protected async Task AddAIResponseAsync(DateTimeOffset time, bool hideContentFromUser = false) { var aiText = new ContentText { @@ -236,6 +278,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver ContentType = ContentType.TEXT, Role = ChatRole.AI, Content = aiText, + HideFromUser = hideContentFromUser, }; if (this.chatThread is not null) @@ -313,7 +356,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver await this.JsRuntime.ClearDiv(RESULT_DIV_ID); await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID); - this.ResetFrom(); + this.ResetForm(); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); this.inputIsValid = false; @@ -341,6 +384,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver public void Dispose() { this.MessageBus.Unregister(this); + this.formChangeTimer.Dispose(); } #endregion diff --git a/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs b/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs index 3ad8cc9..576e433 100644 --- a/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/BiasDay/BiasOfTheDayAssistant.razor.cs @@ -1,6 +1,6 @@ using System.Text; -using AIStudio.Components; +using AIStudio.Chat; using AIStudio.Settings.DataModel; namespace AIStudio.Assistants.BiasDay; @@ -50,7 +50,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore protected override bool ShowReset => false; - protected override void ResetFrom() + protected override void ResetForm() { if (!this.MightPreselectValues()) { @@ -124,7 +124,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore { var biasChat = new LoadChat { - WorkspaceId = Workspaces.WORKSPACE_ID_BIAS, + WorkspaceId = KnownWorkspaces.BIAS_WORKSPACE_ID, ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId, }; @@ -147,7 +147,7 @@ public partial class BiasOfTheDayAssistant : AssistantBaseCore BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] : BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias); - var chatId = this.CreateChatThread(Workspaces.WORKSPACE_ID_BIAS, this.biasOfTheDay.Name); + var chatId = this.CreateChatThread(KnownWorkspaces.BIAS_WORKSPACE_ID, this.biasOfTheDay.Name); this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id; this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId; this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now); diff --git a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs index 1c7f69d..c8a6112 100644 --- a/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs +++ b/app/MindWork AI Studio/Assistants/Coding/AssistantCoding.razor.cs @@ -32,7 +32,7 @@ public partial class AssistantCoding : AssistantBaseCore protected override Func SubmitAction => this.GetSupport; - protected override void ResetFrom() + protected override void ResetForm() { this.codingContexts.Clear(); this.compilerMessages = string.Empty; diff --git a/app/MindWork AI Studio/Assistants/Coding/CodingContextItem.razor b/app/MindWork AI Studio/Assistants/Coding/CodingContextItem.razor index 5c28bb3..e49d4f1 100644 --- a/app/MindWork AI Studio/Assistants/Coding/CodingContextItem.razor +++ b/app/MindWork AI Studio/Assistants/Coding/CodingContextItem.razor @@ -1,5 +1,5 @@  - + @foreach (var language in Enum.GetValues()) { diff --git a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs index 59d5fec..4606f4b 100644 --- a/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs +++ b/app/MindWork AI Studio/Assistants/EMail/AssistantEMail.razor.cs @@ -33,7 +33,7 @@ public partial class AssistantEMail : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputBulletPoints = string.Empty; this.bulletPointsLines.Clear(); diff --git a/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProviders.cs b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProviders.cs new file mode 100644 index 0000000..7bc8f0d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProviders.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Assistants.ERI; + +public enum AllowedLLMProviders +{ + NONE, + + ANY, + SELF_HOSTED, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs new file mode 100644 index 0000000..130859b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/AllowedLLMProvidersExtensions.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Assistants.ERI; + +public static class AllowedLLMProvidersExtensions +{ + public static string Description(this AllowedLLMProviders provider) + { + return provider switch + { + AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source", + AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider", + AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider", + + _ => "Unknown option was selected" + }; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor new file mode 100644 index 0000000..5e008e2 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor @@ -0,0 +1,349 @@ +@attribute [Route(Routes.ASSISTANT_ERI)] +@using AIStudio.Settings.DataModel +@using MudExtensions +@inherits AssistantBaseCore + + + You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize + all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the + vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to + significantly reduce the hallucination of the LLM in knowledge questions. + + + + Related links: + + + ERI repository with example implementation in .NET and C# + Interactive documentation aka Swagger UI + + + +
+ + + ERI server presets + + + + Here you have the option to save different configurations for various ERI servers and switch between them. This is useful if + you are responsible for multiple ERI servers. + + +@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0) +{ + + You have not yet added any ERI server presets. + +} +else +{ + + @foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers) + { + + @server.ServerName + + } + +} + + + + Add ERI server preset + + + Delete this server preset + + + +@if(this.AreServerPresetsBlocked) +{ + + Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings. + +} + + + Auto save + + + + The ERI specification will change over time. You probably want to keep your ERI server up to date. This means you might want to + regenerate the code for your ERI server. To avoid having to make all inputs each time, all your inputs and decisions can be + automatically saved. Would you like this? + + +@if(this.AreServerPresetsBlocked) +{ + + Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings. + +} + + + +
+ + + Common ERI server settings + + + + + + + @foreach (var language in Enum.GetValues()) + { + @language.Name() + } + + @if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER) + { + + } + + + + + @foreach (var version in Enum.GetValues()) + { + @version + } + + + Download specification + + + + + Data source settings + + + + + @foreach (var dataSource in Enum.GetValues()) + { + @dataSource.Name() + } + + @if (this.selectedDataSource is DataSources.CUSTOM) + { + + } + + +@if(this.selectedDataSource > DataSources.FILE_SYSTEM) +{ + +} + +@if (this.NeedHostnamePort()) +{ +
+ + + + + @if (this.dataSourcePort < 1024) + { + + Warning: Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user). + + } +
+} + + + Authentication settings + + + + + @foreach (var authMethod in Enum.GetValues()) + { + @authMethod.Name() + } + + + + +@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS)) +{ + + @foreach (var os in Enum.GetValues()) + { + @os.Name() + } + +} + + + Data protection settings + + + + @foreach (var option in Enum.GetValues()) + { + @option.Description() + } + + + + Embedding settings + + + + You will likely use one or more embedding methods to encode the meaning of your data into a typically high-dimensional vector + space. In this case, you will use a vector database to store and search these vectors (called embeddings). However, you don't + have to use embedding methods. When your retrieval method works without any embedding, you can ignore this section. An example: You + store files on a file server, and your retrieval method works exclusively with file names in the file system, so you don't + need embeddings. + + + + You can specify more than one embedding method. This can be useful when you want to use different embeddings for different queries + or data types. For example, one embedding for texts, another for images, and a third for videos, etc. + + +@if (!this.IsNoneERIServerSelected) +{ + + + + + + + + Name + Type + Actions + + + @context.EmbeddingName + @context.EmbeddingType + + + Edit + + + Delete + + + + + + @if (this.embeddings.Count == 0) + { + No embedding methods configured yet. + } +} + + + Add Embedding Method + + + + 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. + + +@if (!this.IsNoneERIServerSelected) +{ + + + + + + + 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. + It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these + libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe + the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected. + + + + + + Provider selection for generation + + + + The task of writing the ERI server for you is very complex. Therefore, a very powerful LLM is needed to successfully accomplish this task. + Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model. + + + + Important: The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number + of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this. + However, generating all the files takes a certain amount of time. Local or self-hosted models may work without these limitations + and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing. + + + + + + Write code to file system + + + + AI Studio can save the generated code to the file system. You can select a base folder for this. AI Studio ensures that no files are created + outside of this base folder. Furthermore, we recommend that you create a Git repository in this folder. This way, you can see what changes the + AI has made in which files. + + + + When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All + other files you have created remain. Then, the AI generates the new files. But beware: It may happen that the AI generates a + file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore, + you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization, + you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose. + + + + diff --git a/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs new file mode 100644 index 0000000..8534731 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/AssistantERI.razor.cs @@ -0,0 +1,1087 @@ +using System.Text; +using System.Text.RegularExpressions; + +using AIStudio.Chat; +using AIStudio.Dialogs; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Assistants.ERI; + +public partial class AssistantERI : AssistantBaseCore +{ + [Inject] + private HttpClient HttpClient { get; set; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + public override Tools.Components Component => Tools.Components.ERI_ASSISTANT; + + protected override string Title => "ERI Server"; + + protected override string Description => + """ + 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 + { + get + { + var sb = new StringBuilder(); + + // + // --------------------------------- + // Introduction + // --------------------------------- + // + var programmingLanguage = this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER ? this.otherProgrammingLanguage : this.selectedProgrammingLanguage.Name(); + sb.Append($""" + # Introduction + You are an experienced {programmingLanguage} developer. Your task is to implement an API server in + the {programmingLanguage} language according to the following OpenAPI Description (OAD): + + ``` + {this.eriSpecification} + ``` + + The server realizes the data retrieval component for a "Retrieval-Augmentation Generation" (RAG) process. + The server is called "{this.serverName}" and is described as follows: + + ``` + {this.serverDescription} + ``` + """); + + // + // --------------------------------- + // Data Source + // --------------------------------- + // + + sb.Append(""" + + # Data Source + """); + + switch (this.selectedDataSource) + { + case DataSources.CUSTOM: + sb.Append($""" + The data source for the retrieval process is described as follows: + + ``` + {this.otherDataSource} + ``` + """); + + if(!string.IsNullOrWhiteSpace(this.dataSourceHostname)) + { + sb.Append($""" + + The data source is accessible via the hostname `{this.dataSourceHostname}`. + """); + } + + if(this.dataSourcePort is not null) + { + sb.Append($""" + + The data source is accessible via port `{this.dataSourcePort}`. + """); + } + + break; + + case DataSources.FILE_SYSTEM: + sb.Append(""" + + The data source for the retrieval process is the local file system. Use a placeholder for the data source path. + """); + break; + + default: + case DataSources.OBJECT_STORAGE: + case DataSources.KEY_VALUE_STORE: + case DataSources.DOCUMENT_STORE: + case DataSources.RELATIONAL_DATABASE: + case DataSources.GRAPH_DATABASE: + sb.Append($""" + The data source for the retrieval process is an "{this.dataSourceProductName}" database running on the + host `{this.dataSourceHostname}` and is accessible via port `{this.dataSourcePort}`. + """); + break; + } + + // + // --------------------------------- + // Authentication and Authorization + // --------------------------------- + // + + sb.Append(""" + + # Authentication and Authorization + The process for authentication and authorization is two-step. Step 1: Users must authenticate + with the API server using a `POST` call on `/auth` with the chosen method. If this step is + successful, the API server returns a token. Step 2: This token is then required for all + other API calls. + + Important notes: + - Calls to `/auth` and `/auth/methods` are accessible without authentication. All other API + endpoints require a valid step 2 token. + + - It is possible that a token (step 1 token) is desired as `authMethod`. These step 1 tokens + must never be accepted as valid tokens for step 2. + + - The OpenAPI Description (OAD) for `/auth` is not complete. This is because, at the time of + writing the OAD, it is not known what kind of authentication is desired. Therefore, you must + supplement the corresponding fields or data in the implementation. Example: If username/password + is desired, you must expect and read both. If both token and username/password are desired, you + must dynamically read the `authMethod` and expect and evaluate different fields accordingly. + + The following authentications and authorizations should be implemented for the API server: + """); + + foreach (var auth in this.selectedAuthenticationMethods) + sb.Append($"- {auth.ToPrompt()}"); + + if(this.IsUsingKerberos()) + { + sb.Append($""" + + The server will run on {this.selectedOperatingSystem.Name()} operating systems. Keep + this in mind when implementing the SSO with Kerberos. + """); + } + + // + // --------------------------------- + // Security + // --------------------------------- + // + + sb.Append($""" + + # Security + The following security requirement for `allowedProviderType` was chosen: `{this.allowedLLMProviders}` + """); + + // + // --------------------------------- + // Retrieval Processes + // --------------------------------- + // + + sb.Append($""" + + # Retrieval Processes + You are implementing the following data retrieval processes: + """); + + var retrievalProcessCounter = 1; + foreach (var retrievalProcess in this.retrievalProcesses) + { + sb.Append($""" + + ## {retrievalProcessCounter++}. Retrieval Process + - Name: {retrievalProcess.Name} + - Description: + + ``` + {retrievalProcess.Description} + ``` + """); + + if(retrievalProcess.ParametersDescription?.Count > 0) + { + sb.Append(""" + + This retrieval process recognizes the following parameters: + """); + + var parameterCounter = 1; + foreach (var (parameter, description) in retrievalProcess.ParametersDescription) + { + sb.Append($""" + + - The {parameterCounter++} parameter is named "{parameter}": + + ``` + {description} + ``` + """); + } + + sb.Append(""" + + Please use sensible default values for the parameters. They are optional + for the user. + """); + } + + if(retrievalProcess.Embeddings?.Count > 0) + { + sb.Append(""" + + The following embeddings are implemented for this retrieval process: + """); + + var embeddingCounter = 1; + foreach (var embedding in retrievalProcess.Embeddings) + { + sb.Append($""" + + - {embeddingCounter++}. Embedding + - Name: {embedding.EmbeddingName} + - Type: {embedding.EmbeddingType} + + - Description: + + ``` + {embedding.Description} + ``` + + - When used: + + ``` + {embedding.UsedWhen} + ``` + """); + } + } + } + + // + // --------------------------------- + // Additional Libraries + // --------------------------------- + // + + if (!string.IsNullOrWhiteSpace(this.additionalLibraries)) + { + sb.Append($""" + + # Additional Libraries + You use the following libraries for your implementation: + + {this.additionalLibraries} + """); + } + + // + // --------------------------------- + // Remarks + // --------------------------------- + // + + sb.Append(""" + + # Remarks + - You do not ask follow-up questions. + - You consider the security of the implementation by applying the Security by Design principle. + - Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you + create a separate code block with its file path and name as chapter title. + """); + + return sb.ToString(); + } + } + + protected override IReadOnlyList FooterButtons => []; + + protected override bool ShowEntireChatThread => true; + + protected override string SubmitText => "Create the ERI server"; + + protected override Func SubmitAction => this.GenerateServer; + + protected override bool SubmitDisabled => this.IsNoneERIServerSelected; + + protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with + { + SystemPrompt = this.SystemPrompt, + }; + + 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(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 selectedAuthenticationMethods = new HashSet(); + private string authDescription = string.Empty; + private OperatingSystem selectedOperatingSystem = OperatingSystem.NONE; + private AllowedLLMProviders allowedLLMProviders = AllowedLLMProviders.NONE; + private List embeddings = new(); + private List retrievalProcesses = new(); + private string additionalLibraries = string.Empty; + private bool writeToFilesystem; + private string baseDirectory = string.Empty; + private List previouslyGeneratedFiles = new(); + + private bool AreServerPresetsBlocked => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions; + + private void SelectedERIServerChanged(DataERIServer? server) + { + this.selectedERIServer = server; + this.ResetForm(); + } + + private async Task AddERIServer() + { + this.SettingsManager.ConfigurationData.ERI.ERIServers.Add(new () + { + ServerName = $"ERI Server {DateTimeOffset.UtcNow}", + }); + + await this.SettingsManager.StoreSettings(); + } + + private async Task RemoveERIServer() + { + if(this.selectedERIServer is null) + return; + + this.SettingsManager.ConfigurationData.ERI.ERIServers.Remove(this.selectedERIServer); + this.selectedERIServer = null; + this.ResetForm(); + + await this.SettingsManager.StoreSettings(); + this.form?.ResetValidation(); + } + + private bool IsNoneERIServerSelected => this.selectedERIServer is null; + + /// + /// Gets called when the server name was changed by typing. + /// + /// + /// 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. + /// + private void ServerNameWasChanged() + { + if(this.selectedERIServer is null) + return; + + this.selectedERIServer.ServerName = this.serverName; + } + + private string? ValidateServerName(string name) + { + if(string.IsNullOrWhiteSpace(name)) + return "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "Please provide the port of the data source."; + + if(port is < 1 or > 65535) + return "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)!; + if(authenticationMethods.Count == 0) + return "Please select at least one authentication method for the ERI server."; + + return null; + } + + private void AuthenticationMethodWasChanged(IEnumerable? 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 "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 "Please select which types of LLMs users are allowed to use with the data from this ERI server."; + + return null; + } + + private string AuthDescriptionTitle() + { + const string TITLE = "Describe how you planned the authentication process"; + return this.IsAuthDescriptionOptional() ? $"(Optional) {TITLE}" : TITLE; + } + + private bool IsAuthDescriptionOptional() + { + if (this.selectedAuthenticationMethods is not HashSet 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)!; + if(authenticationMethods.Any(n => n == Auth.NONE) && authenticationMethods.Count > 1 && string.IsNullOrWhiteSpace(this.authDescription)) + return "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 "Please describe how the selected authentication methods should be used."; + + return null; + } + + private string GetMultiSelectionAuthText(List selectedValues) + { + if(selectedValues.Count == 0) + return "Please select at least one authentication method"; + + if(selectedValues.Count == 1) + return $"You have selected 1 authentication method"; + + return $"You have selected {selectedValues.Count} authentication methods"; + } + + private bool NeedHostnamePort() + { + switch (this.selectedDataSource) + { + case DataSources.NONE: + case DataSources.FILE_SYSTEM: + return false; + + default: + return true; + } + } + + private async Task AddEmbedding() + { + var dialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + { x => x.UsedEmbeddingMethodNames, this.embeddings.Select(n => n.EmbeddingName).ToList() }, + }; + + var dialogReference = await this.DialogService.ShowAsync("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) + { + var dialogParameters = new DialogParameters + { + { 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("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) + { + var message = this.retrievalProcesses.Any(n => n.Embeddings?.Contains(embeddingInfo) is true) + ? $"The embedding '{embeddingInfo.EmbeddingName}' is used in one or more retrieval processes. Are you sure you want to delete it?" + : $"Are you sure you want to delete the embedding '{embeddingInfo.EmbeddingName}'?"; + + var dialogParameters = new DialogParameters + { + { "Message", message }, + }; + + var dialogReference = await this.DialogService.ShowAsync("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() + { + var dialogParameters = new DialogParameters + { + { 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("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 }, + { x => x.UsedRetrievalProcessNames, this.retrievalProcesses.Where(n => n != retrievalInfo).Select(n => n.Name).ToList() }, + }; + + 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(); + } + + [GeneratedRegex(""" + "([\\/._\w-]+)" + """, RegexOptions.NonBacktracking)] + private static partial Regex FileExtractRegex(); + + private IEnumerable ExtractFiles(string fileListAnswer) + { + // + // We asked the LLM for answering using a specific JSON scheme. + // However, the LLM might not follow this scheme. Therefore, we + // need to parse the answer and extract the files. + // The parsing strategy is to look for all strings. + // + var matches = FileExtractRegex().Matches(fileListAnswer); + foreach (Match match in matches) + if(match.Groups[1].Value is {} file && !string.IsNullOrWhiteSpace(file) && !file.Equals("files", StringComparison.OrdinalIgnoreCase)) + yield return file; + } + + [GeneratedRegex(""" + \s*#+\s+[\w\\/.]+\s*```\w*\s+([\s\w\W]+)\s*```\s* + """, RegexOptions.Singleline)] + private static partial Regex CodeExtractRegex(); + + private string ExtractCode(string markdown) + { + var match = CodeExtractRegex().Match(markdown); + return match.Success ? match.Groups[1].Value : string.Empty; + } + + private async Task GenerateServer() + { + if(this.IsNoneERIServerSelected) + return; + + await this.AutoSave(); + await this.form!.Validate(); + if (!this.inputIsValid) + return; + + if(this.retrievalProcesses.Count == 0) + { + this.AddInputIssue("Please describe at least one retrieval process."); + return; + } + + this.eriSpecification = await this.selectedERIVersion.ReadSpecification(this.HttpClient); + if (string.IsNullOrWhiteSpace(this.eriSpecification)) + { + this.AddInputIssue("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: + + { + [ + "path/to/file1", + "path/to/file2" + ] + } + """, true); + var fileListAnswer = await this.AddAIResponseAsync(time, true); + + // Is this an update of the ERI server? If so, we need to delete the previously generated files: + if (this.writeToFilesystem && this.previouslyGeneratedFiles.Count > 0 && !string.IsNullOrWhiteSpace(fileListAnswer)) + { + foreach (var file in this.previouslyGeneratedFiles) + { + 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(); + foreach (var file in this.ExtractFiles(fileListAnswer)) + { + 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 + ``` + """, true); + var generatedCodeMarkdown = await this.AddAIResponseAsync(time); + if (this.writeToFilesystem) + { + var desiredFilePath = Path.Join(this.baseDirectory, file); + + // Security check: ensure that the desired file path is inside the base directory. + // We cannot trust the beginning of the file path because it would be possible + // to escape by using `..` in the file path. + if (!desiredFilePath.StartsWith(this.baseDirectory, StringComparison.InvariantCultureIgnoreCase) || desiredFilePath.Contains("..")) + this.Logger.LogWarning($"The file path '{desiredFilePath}' is may not inside the base directory '{this.baseDirectory}'."); + + else + { + var code = this.ExtractCode(generatedCodeMarkdown); + if (string.IsNullOrWhiteSpace(code)) + this.Logger.LogWarning($"The file content for '{desiredFilePath}' is empty or was not found."); + + else + { + + // Ensure that the directory exists: + var fileDirectory = Path.GetDirectoryName(desiredFilePath); + if (fileDirectory is null) + this.Logger.LogWarning($"The file path '{desiredFilePath}' does not contain a directory."); + + else + { + generatedFiles.Add(desiredFilePath); + var fileDirectoryInfo = new DirectoryInfo(fileDirectory); + if(!fileDirectoryInfo.Exists) + { + fileDirectoryInfo.Create(); + this.Logger.LogInformation($"The directory '{fileDirectory}' was created."); + } + + // Save the file to the file system: + await File.WriteAllTextAsync(desiredFilePath, code, Encoding.UTF8); + this.Logger.LogInformation($"The file '{desiredFilePath}' was created."); + } + } + } + } + } + + if(this.writeToFilesystem) + { + 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); + await this.SendToAssistant(Tools.Components.CHAT, default); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/Auth.cs b/app/MindWork AI Studio/Assistants/ERI/Auth.cs new file mode 100644 index 0000000..fdb3e5d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/Auth.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Assistants.ERI; + +public enum Auth +{ + NONE, + KERBEROS, + USERNAME_PASSWORD, + TOKEN, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs new file mode 100644 index 0000000..b8a8bdf --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/AuthExtensions.cs @@ -0,0 +1,26 @@ +namespace AIStudio.Assistants.ERI; + +public static class AuthExtensions +{ + public static string Name(this Auth auth) => auth switch + { + Auth.NONE => "No login necessary: useful for public data sources", + + Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users", + Auth.USERNAME_PASSWORD => "Login by username and password: simple to implement and to operate, useful for few users; easy to use for users", + Auth.TOKEN => "Login by token: simple to implement and to operate, useful for few users; unusual for many users", + + _ => "Unknown login method" + }; + + public static string ToPrompt(this Auth auth) => auth switch + { + Auth.NONE => "No login is necessary, the data source is public.", + + Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.", + Auth.USERNAME_PASSWORD => "Login by username and password.", + Auth.TOKEN => "Login by static token per user.", + + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/DataSources.cs b/app/MindWork AI Studio/Assistants/ERI/DataSources.cs new file mode 100644 index 0000000..75391c5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/DataSources.cs @@ -0,0 +1,15 @@ +namespace AIStudio.Assistants.ERI; + +public enum DataSources +{ + NONE, + CUSTOM, + + FILE_SYSTEM, + + OBJECT_STORAGE, + KEY_VALUE_STORE, + DOCUMENT_STORE, + RELATIONAL_DATABASE, + GRAPH_DATABASE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs new file mode 100644 index 0000000..83c5c1d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/DataSourcesExtensions.cs @@ -0,0 +1,19 @@ +namespace AIStudio.Assistants.ERI; + +public static class DataSourcesExtensions +{ + public static string Name(this DataSources dataSource) => dataSource switch + { + DataSources.NONE => "No data source selected", + DataSources.CUSTOM => "Custom description", + + DataSources.FILE_SYSTEM => "File system (local or network share)", + DataSources.OBJECT_STORAGE => "Object storage, like Amazon S3, MinIO, etc.", + DataSources.KEY_VALUE_STORE => "Key-Value store, like Redis, etc.", + DataSources.DOCUMENT_STORE => "Document store, like MongoDB, etc.", + DataSources.RELATIONAL_DATABASE => "Relational database, like MySQL, PostgreSQL, etc.", + DataSources.GRAPH_DATABASE => "Graph database, like Neo4j, ArangoDB, etc.", + + _ => "Unknown data source" + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/ERIVersion.cs b/app/MindWork AI Studio/Assistants/ERI/ERIVersion.cs new file mode 100644 index 0000000..24d0711 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/ERIVersion.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Assistants.ERI; + +public enum ERIVersion +{ + NONE, + + V1, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs new file mode 100644 index 0000000..c7ef7c1 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/ERIVersionExtensions.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Assistants.ERI; + +public static class ERIVersionExtensions +{ + public static async Task ReadSpecification(this ERIVersion version, HttpClient httpClient) + { + try + { + var url = version.SpecificationURL(); + var response = await httpClient.GetAsync(url); + return await response.Content.ReadAsStringAsync(); + } + catch + { + return string.Empty; + } + } + + public static string SpecificationURL(this ERIVersion version) + { + var nameLower = version.ToString().ToLowerInvariant(); + var filename = $"{nameLower}.json"; + return $"specs/eri/{filename}"; + } + + public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/EmbeddingInfo.cs b/app/MindWork AI Studio/Assistants/ERI/EmbeddingInfo.cs new file mode 100644 index 0000000..938ac20 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/EmbeddingInfo.cs @@ -0,0 +1,19 @@ +namespace AIStudio.Assistants.ERI; + +/// +/// Represents information about the used embedding for a data source. +/// +/// What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word +/// Embedding," "Graph Embedding," etc. +/// Name the embedding used. This can be a library, a framework, or the name of the used +/// algorithm. +/// A short description of the embedding. Describe what the embedding is doing. +/// Describe when the embedding is used. For example, when the user prompt contains certain +/// keywords, or anytime? +/// A link to the embedding's documentation or the source code. Might be null. +public readonly record struct EmbeddingInfo( + string EmbeddingType, + string EmbeddingName, + string Description, + string UsedWhen, + string? Link); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/OperatingSystem.cs b/app/MindWork AI Studio/Assistants/ERI/OperatingSystem.cs new file mode 100644 index 0000000..377fb4c --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/OperatingSystem.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Assistants.ERI; + +public enum OperatingSystem +{ + NONE, + + WINDOWS, + LINUX, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs new file mode 100644 index 0000000..eac4614 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/OperatingSystemExtensions.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Assistants.ERI; + +public static class OperatingSystemExtensions +{ + public static string Name(this OperatingSystem os) => os switch + { + OperatingSystem.NONE => "No operating system specified", + + OperatingSystem.WINDOWS => "Windows", + OperatingSystem.LINUX => "Linux", + + _ => "Unknown operating system" + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguages.cs b/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguages.cs new file mode 100644 index 0000000..1c7c8bb --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguages.cs @@ -0,0 +1,20 @@ +namespace AIStudio.Assistants.ERI; + +public enum ProgrammingLanguages +{ + NONE, + + C, + CPP, + CSHARP, + GO, + JAVA, + JAVASCRIPT, + JULIA, + MATLAB, + PHP, + PYTHON, + RUST, + + OTHER, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs b/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs new file mode 100644 index 0000000..f4b8be6 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/ERI/ProgrammingLanguagesExtensions.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Assistants.ERI; + +public static class ProgrammingLanguagesExtensions +{ + public static string Name(this ProgrammingLanguages language) => language switch + { + ProgrammingLanguages.NONE => "No programming language selected", + + ProgrammingLanguages.C => "C", + ProgrammingLanguages.CPP => "C++", + ProgrammingLanguages.CSHARP => "C#", + ProgrammingLanguages.GO => "Go", + ProgrammingLanguages.JAVA => "Java", + ProgrammingLanguages.JAVASCRIPT => "JavaScript", + ProgrammingLanguages.JULIA => "Julia", + ProgrammingLanguages.MATLAB => "MATLAB", + ProgrammingLanguages.PHP => "PHP", + ProgrammingLanguages.PYTHON => "Python", + ProgrammingLanguages.RUST => "Rust", + + ProgrammingLanguages.OTHER => "Other", + _ => "Unknown" + }; +} \ No newline at end of file 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 0000000..78ff7d2 --- /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 0000000..d25a5bd --- /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/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs index f112dd1..29cf486 100644 --- a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs +++ b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs @@ -48,7 +48,7 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputText = string.Empty; this.correctedText = string.Empty; diff --git a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs index 6b7ace7..5434834 100644 --- a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs +++ b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs @@ -33,7 +33,7 @@ public partial class AssistantIconFinder : AssistantBaseCore protected override Func SubmitAction => this.FindIcon; - protected override void ResetFrom() + protected override void ResetForm() { this.inputContext = string.Empty; if (!this.MightPreselectValues()) diff --git a/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs b/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs index 7211df6..164abde 100644 --- a/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs +++ b/app/MindWork AI Studio/Assistants/JobPosting/AssistantJobPostings.razor.cs @@ -59,7 +59,7 @@ public partial class AssistantJobPostings : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputEntryDate = string.Empty; this.inputValidUntil = string.Empty; diff --git a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs index 74941ca..4575567 100644 --- a/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs +++ b/app/MindWork AI Studio/Assistants/LegalCheck/AssistantLegalCheck.razor.cs @@ -37,7 +37,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputLegalDocument = string.Empty; this.inputQuestions = string.Empty; diff --git a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs index 1d1e38a..b0e52a1 100644 --- a/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs +++ b/app/MindWork AI Studio/Assistants/MyTasks/AssistantMyTasks.razor.cs @@ -41,7 +41,7 @@ public partial class AssistantMyTasks : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputText = string.Empty; if (!this.MightPreselectValues()) diff --git a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs index 394deae..216f85a 100644 --- a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs +++ b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs @@ -49,7 +49,7 @@ public partial class AssistantRewriteImprove : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputText = string.Empty; this.rewrittenText = string.Empty; diff --git a/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs b/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs index e99c6f3..c849b68 100644 --- a/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs +++ b/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs @@ -60,7 +60,7 @@ public partial class AssistantSynonyms : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputText = string.Empty; this.inputContext = string.Empty; diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs index 02c222d..fb9244b 100644 --- a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs @@ -40,7 +40,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputText = string.Empty; if(!this.MightPreselectValues()) diff --git a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs index 8263112..785aae4 100644 --- a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs +++ b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs @@ -36,7 +36,7 @@ public partial class AssistantTranslation : AssistantBaseCore SystemPrompt = SystemPrompts.DEFAULT, }; - protected override void ResetFrom() + protected override void ResetForm() { this.inputText = string.Empty; this.inputTextLastTranslation = string.Empty; diff --git a/app/MindWork AI Studio/Chat/KnownWorkspaces.cs b/app/MindWork AI Studio/Chat/KnownWorkspaces.cs new file mode 100644 index 0000000..eee1bbd --- /dev/null +++ b/app/MindWork AI Studio/Chat/KnownWorkspaces.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Chat; + +public static class KnownWorkspaces +{ + public static readonly Guid BIAS_WORKSPACE_ID = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02"); + public static readonly Guid ERI_SERVER_WORKSPACE_ID = Guid.Parse("8ec09cd3-9da7-4736-b245-2d8b67fc342f"); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/EnumSelection.razor b/app/MindWork AI Studio/Components/EnumSelection.razor index 144d0ea..84fd613 100644 --- a/app/MindWork AI Studio/Components/EnumSelection.razor +++ b/app/MindWork AI Studio/Components/EnumSelection.razor @@ -1,7 +1,7 @@ @typeparam T @inherits EnumSelectionBase - + @foreach (var value in Enum.GetValues()) { diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor b/app/MindWork AI Studio/Components/SelectDirectory.razor new file mode 100644 index 0000000..95f09d6 --- /dev/null +++ b/app/MindWork AI Studio/Components/SelectDirectory.razor @@ -0,0 +1,16 @@ + + + + + Choose Directory + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/SelectDirectory.razor.cs b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs new file mode 100644 index 0000000..ec4f6cd --- /dev/null +++ b/app/MindWork AI Studio/Components/SelectDirectory.razor.cs @@ -0,0 +1,60 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class SelectDirectory : ComponentBase +{ + [Parameter] + public string Directory { get; set; } = string.Empty; + + [Parameter] + public EventCallback DirectoryChanged { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public string DirectoryDialogTitle { get; set; } = "Select Directory"; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + [Inject] + public RustService RustService { get; set; } = null!; + + [Inject] + protected ILogger Logger { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + await base.OnInitializedAsync(); + } + + #endregion + + private void InternalDirectoryChanged(string directory) + { + this.Directory = directory; + this.DirectoryChanged.InvokeAsync(directory); + } + + private async Task OpenDirectoryDialog() + { + var response = await this.RustService.SelectDirectory(this.DirectoryDialogTitle, string.IsNullOrWhiteSpace(this.Directory) ? null : this.Directory); + this.Logger.LogInformation($"The user selected the directory '{response.SelectedDirectory}'."); + + if (!response.UserCancelled) + this.InternalDirectoryChanged(response.SelectedDirectory); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Workspaces.razor.cs b/app/MindWork AI Studio/Components/Workspaces.razor.cs index 84181db..2f96f02 100644 --- a/app/MindWork AI Studio/Components/Workspaces.razor.cs +++ b/app/MindWork AI Studio/Components/Workspaces.razor.cs @@ -38,8 +38,6 @@ public partial class Workspaces : ComponentBase public bool ExpandRootNodes { get; set; } = true; private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom; - - public static readonly Guid WORKSPACE_ID_BIAS = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02"); private readonly List> treeItems = new(); @@ -53,8 +51,6 @@ public partial class Workspaces : ComponentBase // - Those initial tree items cannot have children // - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item // - - await this.EnsureBiasWorkspace(); await this.LoadTreeItems(); await base.OnInitializedAsync(); } @@ -408,18 +404,6 @@ public partial class Workspaces : ComponentBase await this.LoadTreeItems(); } - private async Task EnsureBiasWorkspace() - { - var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", WORKSPACE_ID_BIAS.ToString()); - - if(Path.Exists(workspacePath)) - return; - - Directory.CreateDirectory(workspacePath); - var workspaceNamePath = Path.Join(workspacePath, "name"); - await File.WriteAllTextAsync(workspaceNamePath, "Bias of the Day", Encoding.UTF8); - } - private async Task DeleteWorkspace(string? workspacePath) { if(workspacePath is null) diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor new file mode 100644 index 0000000..5d8da89 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor @@ -0,0 +1,109 @@ + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + + + Cancel + + @if(this.IsEditing) + { + @:Update + } + else + { + @:Add + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs new file mode 100644 index 0000000..c2733fb --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/EmbeddingMethodDialog.razor.cs @@ -0,0 +1,144 @@ +using AIStudio.Assistants.ERI; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class EmbeddingMethodDialog : ComponentBase +{ + [CascadingParameter] + private MudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The user chosen embedding name. + /// + [Parameter] + public string DataEmbeddingName { get; set; } = string.Empty; + + /// + /// The user chosen embedding type. + /// + [Parameter] + public string DataEmbeddingType { get; set; } = string.Empty; + + /// + /// The embedding description. + /// + [Parameter] + public string DataDescription { get; set; } = string.Empty; + + /// + /// When is the embedding used? + /// + [Parameter] + public string DataUsedWhen { get; set; } = string.Empty; + + /// + /// A link to the embedding documentation or the source code. Might be null, which means no link is provided. + /// + [Parameter] + public string DataLink { get; set; } = string.Empty; + + /// + /// The embedding method names that are already used. The user must choose a unique name. + /// + [Parameter] + public IReadOnlyList UsedEmbeddingMethodNames { 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 = []; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + private EmbeddingInfo CreateEmbeddingInfo() => new(this.DataEmbeddingType, this.DataEmbeddingName, this.DataDescription, this.DataUsedWhen, this.DataLink); + + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + // Configure the spellchecking for the instance name input: + this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES); + + 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 embedding name must not be empty. Please name the embedding."; + + if (name.Length > 26) + return "The embedding name must not be longer than 26 characters."; + + if (this.UsedEmbeddingMethodNames.Contains(name)) + return $"The embedding method name '{name}' is already used. Please choose a unique name."; + + return null; + } + + private string? ValidateType(string type) + { + if (string.IsNullOrWhiteSpace(type)) + return "The embedding type must not be empty. Please specify the embedding type."; + + if (type.Length > 56) + return "The embedding type must not be longer than 56 characters."; + + return null; + } + + private string? ValidateDescription(string description) + { + if (string.IsNullOrWhiteSpace(description)) + return "The description must not be empty. Please describe the embedding method."; + + return null; + } + + private string? ValidateUsedWhen(string usedWhen) + { + if (string.IsNullOrWhiteSpace(usedWhen)) + return "Please describe when the embedding is used. Might be anytime or when certain keywords are present, etc."; + + return null; + } + + private async Task Store() + { + await this.form.Validate(); + + // When the data is not valid, we don't store it: + if (!this.dataIsValid) + return; + + var embeddingInfo = this.CreateEmbeddingInfo(); + this.MudDialog.Close(DialogResult.Ok(embeddingInfo)); + } + + private void Cancel() => this.MudDialog.Cancel(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor similarity index 100% rename from app/MindWork AI Studio/Dialogs/EmbeddingDialog.razor rename to app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs similarity index 98% rename from app/MindWork AI Studio/Dialogs/EmbeddingDialog.razor.cs rename to app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index 5494ac0..3a7b092 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -8,7 +8,7 @@ using Host = AIStudio.Provider.SelfHosted.Host; namespace AIStudio.Dialogs; -public partial class EmbeddingDialog : ComponentBase, ISecretId +public partial class EmbeddingProviderDialog : ComponentBase, ISecretId { [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!; @@ -97,7 +97,7 @@ public partial class EmbeddingDialog : ComponentBase, ISecretId private readonly Encryption encryption = Program.ENCRYPTION; private readonly ProviderValidation providerValidation; - public EmbeddingDialog() + public EmbeddingProviderDialog() { this.providerValidation = new() { diff --git a/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor new file mode 100644 index 0000000..fb7e3b0 --- /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 0000000..df01a7c --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/RetrievalProcessDialog.razor.cs @@ -0,0 +1,213 @@ +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(); + + /// + /// The retrieval process names that are already used. The user must choose a unique name. + /// + [Parameter] + public IReadOnlyList UsedRetrievalProcessNames { 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."; + + if (this.UsedRetrievalProcessNames.Contains(name)) + return $"The retrieval process name '{name}' must be unique. Please choose a different name."; + + 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/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index 565469a..212f8b5 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -1,3 +1,4 @@ +@using AIStudio.Settings.DataModel @attribute [Route(Routes.ASSISTANTS)] @@ -41,6 +42,10 @@ + @if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager)) + { + + }
\ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Assistants.razor.cs b/app/MindWork AI Studio/Pages/Assistants.razor.cs index 0a8b343..ef2af6d 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor.cs +++ b/app/MindWork AI Studio/Pages/Assistants.razor.cs @@ -1,5 +1,11 @@ +using AIStudio.Settings; + using Microsoft.AspNetCore.Components; namespace AIStudio.Pages; -public partial class Assistants : ComponentBase; \ No newline at end of file +public partial class Assistants : ComponentBase +{ + [Inject] + public SettingsManager SettingsManager { get; set; } = null!; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index fd87a0b..74d27a7 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -60,7 +60,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable // Configure the spellchecking for the user input: this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); - + this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages(Event.SEND_TO_CHAT).FirstOrDefault(); if (deferredContent is not null) @@ -85,6 +85,13 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable { this.autoSaveEnabled = true; this.mustStoreChat = true; + + // Ensure the workspace exists: + if(this.chatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID) + await WorkspaceBehaviour.EnsureERIServerWorkspace(); + + else if (this.chatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID) + await WorkspaceBehaviour.EnsureBiasWorkspace(); } } diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index 1dcae9e..27e762e 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -327,11 +327,28 @@ { } - + + + + + + + + + + Most ERI server options can be customized and saved directly in the ERI server assistant. + For this, the ERI server assistant has an auto-save function. + + + + Switch to ERI server assistant + + + diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs index 25f4acc..eb43631 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor.cs +++ b/app/MindWork AI Studio/Pages/Settings.razor.cs @@ -183,12 +183,12 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable private async Task AddEmbeddingProvider() { - var dialogParameters = new DialogParameters + var dialogParameters = new DialogParameters { { x => x.IsEditing, false }, }; - var dialogReference = await this.DialogService.ShowAsync("Add Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN); + var dialogReference = await this.DialogService.ShowAsync("Add Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) return; @@ -205,7 +205,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable private async Task EditEmbeddingProvider(EmbeddingProvider embeddingProvider) { - var dialogParameters = new DialogParameters + var dialogParameters = new DialogParameters { { x => x.DataNum, embeddingProvider.Num }, { x => x.DataId, embeddingProvider.Id }, @@ -218,7 +218,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable { x => x.DataHost, embeddingProvider.Host }, }; - var dialogReference = await this.DialogService.ShowAsync("Edit Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN); + var dialogReference = await this.DialogService.ShowAsync("Edit Embedding Provider", dialogParameters, DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; if (dialogResult is null || dialogResult.Canceled) return; diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index dc9767d..7c85ff1 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -58,26 +58,33 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap // Right now, we only support streaming completions: Stream = true, }, JSON_SERIALIZER_OPTIONS); - - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "messages"); - - // Set the authorization header: - request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the Anthropic version: - request.Headers.Add("anthropic-version", "2023-06-01"); - - // Set the content: - request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "messages"); + + // Set the authorization header: + request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the Anthropic version: + request.Headers.Add("anthropic-version", "2023-06-01"); + + // Set the content: + request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json"); + return request; + } + + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"Anthropic chat completion failed: {responseData.ErrorMessage}"); + yield break; + } // Open the response stream: - var stream = await response.Content.ReadAsStreamAsync(token); + var stream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: var streamReader = new StreamReader(stream); diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index e52d0c2..32ee685 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -74,4 +74,47 @@ public abstract class BaseProvider : IProvider, ISecretId public string SecretName => this.InstanceName; #endregion + + /// + /// Sends a request and handles rate limiting by exponential backoff. + /// + /// A function that builds the request. + /// The cancellation token. + /// The status object of the request. + protected async Task SendRequest(Func> requestBuilder, CancellationToken token = default) + { + const int MAX_RETRIES = 6; + const double RETRY_DELAY_SECONDS = 4; + + var retry = 0; + var response = default(HttpResponseMessage); + var errorMessage = string.Empty; + while (retry++ < MAX_RETRIES) + { + using var request = await requestBuilder(); + + // Send the request with the ResponseHeadersRead option. + // This allows us to read the stream as soon as the headers are received. + // This is important because we want to stream the responses. + var nextResponse = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + if (nextResponse.IsSuccessStatusCode) + { + response = nextResponse; + break; + } + + errorMessage = nextResponse.ReasonPhrase; + var timeSeconds = Math.Pow(RETRY_DELAY_SECONDS, retry + 1); + if(timeSeconds > 90) + timeSeconds = 90; + + this.logger.LogDebug($"Failed request with status code {nextResponse.StatusCode} (message = '{errorMessage}'). Retrying in {timeSeconds:0.00} seconds."); + await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token); + } + + if(retry >= MAX_RETRIES) + return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response); + + return new HttpRateLimitedStreamResult(true, false, string.Empty, response); + } } \ 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 709aad1..3ab0c50 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -68,22 +68,29 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew Stream = true, }, JSON_SERIALIZER_OPTIONS); - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); - - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"Fireworks chat completion failed: {responseData.ErrorMessage}"); + yield break; + } // Open the response stream: - var fireworksStream = await response.Content.ReadAsStreamAsync(token); + var fireworksStream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: var streamReader = new StreamReader(fireworksStream); diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index 6ca6d92..e3ec173 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -69,22 +69,29 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela Stream = true, }, JSON_SERIALIZER_OPTIONS); - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); - - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(geminiChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"Google chat completion failed: {responseData.ErrorMessage}"); + yield break; + } // Open the response stream: - var geminiStream = await response.Content.ReadAsStreamAsync(token); + var geminiStream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: var streamReader = new StreamReader(geminiStream); diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 477f9a0..5d0fed8 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -71,22 +71,29 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o Stream = true, }, JSON_SERIALIZER_OPTIONS); - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); - - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(groqChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"Groq chat completion failed: {responseData.ErrorMessage}"); + yield break; + } // Open the response stream: - var groqStream = await response.Content.ReadAsStreamAsync(token); + var groqStream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: var streamReader = new StreamReader(groqStream); diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 633fa94..b51778a 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -70,22 +70,29 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api. SafePrompt = false, }, JSON_SERIALIZER_OPTIONS); - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); - - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"Mistral chat completion failed: {responseData.ErrorMessage}"); + yield break; + } // Open the response stream: - var mistralStream = await response.Content.ReadAsStreamAsync(token); + var mistralStream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: var streamReader = new StreamReader(mistralStream); diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 2f1c25a..767d4fe 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -74,22 +74,29 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o FrequencyPenalty = 0f, }, JSON_SERIALIZER_OPTIONS); - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); - - // Set the authorization header: - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - - // Set the content: - request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json"); + return request; + } + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"OpenAI chat completion failed: {responseData.ErrorMessage}"); + yield break; + } + // Open the response stream: - var openAIStream = await response.Content.ReadAsStreamAsync(token); + var openAIStream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: var streamReader = new StreamReader(openAIStream); diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 46958e9..ec81247 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -69,23 +69,30 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam StreamReader? streamReader = default; try { - // Build the HTTP post request: - var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL()); + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL()); - // Set the authorization header: - if (requestedSecret.Success) - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + // Set the authorization header: + if (requestedSecret.Success) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - // Set the content: - request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); - - // Send the request with the ResponseHeadersRead option. - // This allows us to read the stream as soon as the headers are received. - // This is important because we want to stream the responses. - var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); + // Set the content: + request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + // Send the request using exponential backoff: + using var responseData = await this.SendRequest(RequestBuilder, token); + if(responseData.IsFailedAfterAllRetries) + { + this.logger.LogError($"Self-hosted provider's chat completion failed: {responseData.ErrorMessage}"); + yield break; + } // Open the response stream: - var providerStream = await response.Content.ReadAsStreamAsync(token); + var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token); // Add a stream reader to read the stream, line by line: streamReader = new StreamReader(providerStream); diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs index ea1a2d6..9832682 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -24,5 +24,6 @@ public sealed partial class Routes public const string ASSISTANT_MY_TASKS = "/assistant/my-tasks"; public const string ASSISTANT_JOB_POSTING = "/assistant/job-posting"; public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day"; + public const string ASSISTANT_ERI = "/assistant/eri"; // ReSharper restore InconsistentNaming } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index e2b678e..f6f08a5 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -57,6 +57,8 @@ public sealed class Data public DataTranslation Translation { get; init; } = new(); public DataCoding Coding { get; init; } = new(); + + public DataERI ERI { get; init; } = new(); public DataTextSummarizer TextSummarizer { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataERI.cs b/app/MindWork AI Studio/Settings/DataModel/DataERI.cs new file mode 100644 index 0000000..20596a6 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataERI.cs @@ -0,0 +1,36 @@ +using AIStudio.Provider; + +namespace AIStudio.Settings.DataModel; + +public sealed class DataERI +{ + /// + /// Should we automatically save any input made in the ERI assistant? + /// + public bool AutoSaveChanges { get; set; } = true; + + /// + /// Preselect any ERI options? + /// + public bool PreselectOptions { get; set; } = true; + + /// + /// Data for the ERI servers. + /// + public List ERIServers { get; set; } = new(); + + /// + /// The minimum confidence level required for a provider to be considered. + /// + public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE; + + /// + /// Which coding provider should be preselected? + /// + public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataERIServer.cs b/app/MindWork AI Studio/Settings/DataModel/DataERIServer.cs new file mode 100644 index 0000000..1589383 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataERIServer.cs @@ -0,0 +1,113 @@ +using AIStudio.Assistants.ERI; + +using OperatingSystem = AIStudio.Assistants.ERI.OperatingSystem; + +namespace AIStudio.Settings.DataModel; + +public sealed class DataERIServer +{ + /// + /// Preselect the server name? + /// + public string ServerName { get; set; } = string.Empty; + + /// + /// Preselect the server description? + /// + public string ServerDescription { get; set; } = string.Empty; + + /// + /// Preselect the ERI version? + /// + public ERIVersion ERIVersion { get; set; } = ERIVersion.NONE; + + /// + /// Preselect the language for implementing the ERI? + /// + public ProgrammingLanguages ProgrammingLanguage { get; set; } + + /// + /// Do you want to preselect any other language? + /// + public string OtherProgrammingLanguage { get; set; } = string.Empty; + + /// + /// Preselect a data source? + /// + public DataSources DataSource { get; set; } + + /// + /// Do you want to preselect a product name for the data source? + /// + public string DataSourceProductName { get; set; } = string.Empty; + + /// + /// Do you want to preselect any other data source? + /// + public string OtherDataSource { get; set; } = string.Empty; + + /// + /// Do you want to preselect a hostname for the data source? + /// + public string DataSourceHostname { get; set; } = string.Empty; + + /// + /// Do you want to preselect a port for the data source? + /// + public int? DataSourcePort { get; set; } + + /// + /// Did the user type the port number? + /// + public bool UserTypedPort { get; set; } = false; + + /// + /// Preselect any authentication methods? + /// + public HashSet AuthMethods { get; set; } = []; + + /// + /// Do you want to preselect any authentication description? + /// + public string AuthDescription { get; set; } = string.Empty; + + /// + /// Do you want to preselect an operating system? This is necessary when SSO with Kerberos is used. + /// + public OperatingSystem OperatingSystem { get; set; } = OperatingSystem.NONE; + + /// + /// Do you want to preselect which LLM providers are allowed? + /// + public AllowedLLMProviders AllowedLLMProviders { get; set; } = AllowedLLMProviders.NONE; + + /// + /// Do you want to predefine any embedding information? + /// + public List EmbeddingInfos { get; set; } = new(); + + /// + /// Do you want to predefine any retrieval information? + /// + public List RetrievalInfos { get; set; } = new(); + + /// + /// Do you want to preselect any additional libraries? + /// + public string AdditionalLibraries { get; set; } = string.Empty; + + /// + /// Do you want to write all generated code to the filesystem? + /// + public bool WriteToFilesystem { get; set; } + + /// + /// The base directory where to write the generated code to. + /// + public string BaseDirectory { get; set; } = string.Empty; + + /// + /// We save which files were generated previously. + /// + public List PreviouslyGeneratedFiles { get; set; } = new(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Components.cs b/app/MindWork AI Studio/Tools/Components.cs index 30dee58..c910b7c 100644 --- a/app/MindWork AI Studio/Tools/Components.cs +++ b/app/MindWork AI Studio/Tools/Components.cs @@ -17,6 +17,7 @@ public enum Components MY_TASKS_ASSISTANT, JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, + ERI_ASSISTANT, CHAT, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs index cfe1c3d..4374b27 100644 --- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs +++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs @@ -8,6 +8,7 @@ public static class ComponentsExtensions public static bool AllowSendTo(this Components component) => component switch { Components.NONE => false, + Components.ERI_ASSISTANT => false, Components.BIAS_DAY_ASSISTANT => false, _ => true, @@ -27,6 +28,7 @@ public static class ComponentsExtensions Components.SYNONYMS_ASSISTANT => "Synonym Assistant", Components.MY_TASKS_ASSISTANT => "My Tasks Assistant", Components.JOB_POSTING_ASSISTANT => "Job Posting Assistant", + Components.ERI_ASSISTANT => "ERI Server", Components.CHAT => "New Chat", @@ -68,6 +70,7 @@ public static class ComponentsExtensions Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default, Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default, _ => default, }; @@ -87,6 +90,7 @@ public static class ComponentsExtensions Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : default, Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : default, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProvider) : default, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProvider) : default, @@ -101,6 +105,7 @@ public static class ComponentsExtensions Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.LegalCheck.PreselectedProfile) : default, Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.MyTasks.PreselectedProfile) : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile) : default, + Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.ERI.PreselectedProfile) : default, Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == settingsManager.ConfigurationData.Chat.PreselectedProfile) : default, diff --git a/app/MindWork AI Studio/Tools/HttpRateLimitedStreamResult.cs b/app/MindWork AI Studio/Tools/HttpRateLimitedStreamResult.cs new file mode 100644 index 0000000..1e02c86 --- /dev/null +++ b/app/MindWork AI Studio/Tools/HttpRateLimitedStreamResult.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools; + +/// +/// The result of a rate-limited HTTP stream. +/// +/// True, when the stream failed after all retries. +/// The error message which we might show to the user. +/// The response from the server. +public readonly record struct HttpRateLimitedStreamResult( + bool IsSuccessful, + bool IsFailedAfterAllRetries, + string ErrorMessage, + HttpResponseMessage? Response) : IDisposable +{ + #region IDisposable + + public void Dispose() + { + this.Response?.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/DirectorySelectionResponse.cs b/app/MindWork AI Studio/Tools/Rust/DirectorySelectionResponse.cs new file mode 100644 index 0000000..7d874fd --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/DirectorySelectionResponse.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.Rust; + +/// +/// Data structure for selecting a directory. +/// +/// Was the directory selection canceled? +/// The selected directory, if any. +public readonly record struct DirectorySelectionResponse(bool UserCancelled, string SelectedDirectory); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/PreviousDirectory.cs b/app/MindWork AI Studio/Tools/Rust/PreviousDirectory.cs new file mode 100644 index 0000000..b042293 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/PreviousDirectory.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.Rust; + +/// +/// Data structure for selecting a directory when a previous directory was selected. +/// +/// The path of the previous directory. +public readonly record struct PreviousDirectory(string Path); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/RustService.cs b/app/MindWork AI Studio/Tools/RustService.cs index a38cf5f..99476e5 100644 --- a/app/MindWork AI Studio/Tools/RustService.cs +++ b/app/MindWork AI Studio/Tools/RustService.cs @@ -322,6 +322,19 @@ public sealed class RustService : IDisposable return state; } + + public async Task SelectDirectory(string title, string? initialDirectory = null) + { + PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory); + var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'"); + return new DirectorySelectionResponse(true, string.Empty); + } + + return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + } #region IDisposable diff --git a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs index 942374a..a054232 100644 --- a/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs +++ b/app/MindWork AI Studio/Tools/WorkspaceBehaviour.cs @@ -117,4 +117,20 @@ public static class WorkspaceBehaviour Directory.Delete(chatDirectory, true); } + + private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName) + { + var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString()); + + if(Path.Exists(workspacePath)) + return; + + Directory.CreateDirectory(workspacePath); + var workspaceNamePath = Path.Join(workspacePath, "name"); + await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8); + } + + public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day"); + + public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers"); } \ No newline at end of file diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 636821c..6dd2305 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -207,6 +207,6 @@ "contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ==" } }, - "net8.0/osx-x64": {} + "net8.0/osx-arm64": {} } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md new file mode 100644 index 0000000..e1c4498 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.23.md @@ -0,0 +1,6 @@ +# v0.9.23, build 198 (2024-12-xx xx:xx UTC) +- Added an ERI server coding assistant as a preview feature behind the RAG feature flag. This helps you implement an ERI server to gain access to, e.g., your enterprise data from within AI Studio. +- Improved provider requests by handling rate limits by retrying requests. +- Improved the creation of the "the bias of the day" workspace; create that workspace only when the bias of the day feature is used. +- Fixed layout issues when selecting `other` items (e.g., programming languages). +- Fixed a bug about the bias of the day workspace when the workspace component was hidden. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/specs/eri/v1.json b/app/MindWork AI Studio/wwwroot/specs/eri/v1.json new file mode 100644 index 0000000..4159bd7 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/specs/eri/v1.json @@ -0,0 +1,531 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "ERI - (E)xternal (R)etrieval (I)nterface", + "description": "This API serves as a contract between LLM tools like AI Studio and any external data sources for RAG\n(retrieval-augmented generation). The tool, e.g., AI Studio acts as the client (the augmentation and\ngeneration parts) and the data sources act as the server (the retrieval part). The data\nsources implement some form of data retrieval and return a suitable context to the LLM tool.\nThe LLM tool, in turn, handles the integration of appropriate LLMs (augmentation & generation).\nData sources can be document or graph databases, or even a file system, for example. They\nwill likely implement an appropriate retrieval process by using some kind of embedding.\nHowever, this API does not inherently require any embedding, as data processing is\nimplemented decentralized by the data sources.", + "version": "v1" + }, + "paths": { + "/auth/methods": { + "get": { + "tags": [ + "Authentication" + ], + "description": "Get the available authentication methods.", + "operationId": "GetAuthMethods", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthScheme" + } + } + } + } + } + } + } + }, + "/auth": { + "post": { + "tags": [ + "Authentication" + ], + "description": "Authenticate with the data source to get a token for further requests.", + "operationId": "Authenticate", + "parameters": [ + { + "name": "authMethod", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/AuthMethod" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponse" + } + } + } + } + } + } + }, + "/dataSource": { + "get": { + "tags": [ + "Data Source" + ], + "description": "Get information about the data source.", + "operationId": "GetDataSourceInfo", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataSourceInfo" + } + } + } + } + } + } + }, + "/embedding/info": { + "get": { + "tags": [ + "Embedding" + ], + "description": "Get information about the used embedding(s).", + "operationId": "GetEmbeddingInfo", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmbeddingInfo" + } + } + } + } + } + } + } + }, + "/retrieval/info": { + "get": { + "tags": [ + "Retrieval" + ], + "description": "Get information about the retrieval processes implemented by this data source.", + "operationId": "GetRetrievalInfo", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RetrievalInfo" + } + } + } + } + } + } + } + }, + "/retrieval": { + "post": { + "tags": [ + "Retrieval" + ], + "description": "Retrieve information from the data source.", + "operationId": "Retrieve", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrievalRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Context" + } + } + } + } + } + } + } + }, + "/security/requirements": { + "get": { + "tags": [ + "Security" + ], + "description": "Get the security requirements for this data source.", + "operationId": "GetSecurityRequirements", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SecurityRequirements" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AuthField": { + "enum": [ + "NONE", + "USERNAME", + "PASSWORD", + "TOKEN", + "KERBEROS_TICKET" + ], + "type": "string", + "description": "An authentication field." + }, + "AuthFieldMapping": { + "type": "object", + "properties": { + "authField": { + "$ref": "#/components/schemas/AuthField" + }, + "fieldName": { + "type": "string", + "description": "The field name in the authentication request.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The mapping between an AuthField and the field name in the authentication request." + }, + "AuthMethod": { + "enum": [ + "NONE", + "KERBEROS", + "USERNAME_PASSWORD", + "TOKEN" + ], + "type": "string" + }, + "AuthResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "True, when the authentication was successful." + }, + "token": { + "type": "string", + "description": "The token to use for further requests.", + "nullable": true + }, + "message": { + "type": "string", + "description": "When the authentication was not successful, this contains the reason.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "The response to an authentication request." + }, + "AuthScheme": { + "type": "object", + "properties": { + "authMethod": { + "$ref": "#/components/schemas/AuthMethod" + }, + "authFieldMappings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthFieldMapping" + }, + "description": "A list of field mappings for the authentication method. The client must know,\r\n e.g., how the password field is named in the request.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Describes one authentication scheme for this data source." + }, + "ChatThread": { + "type": "object", + "properties": { + "contentBlocks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentBlock" + }, + "description": "The content blocks in this chat thread.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "A chat thread, which is a list of content blocks." + }, + "ContentBlock": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The content of the block. Remember that images and other media are base64 encoded.", + "nullable": true + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "type": { + "$ref": "#/components/schemas/ContentType" + } + }, + "additionalProperties": false, + "description": "A block of content of a chat thread." + }, + "ContentType": { + "enum": [ + "NONE", + "UNKNOWN", + "TEXT", + "IMAGE", + "VIDEO", + "AUDIO", + "SPEECH" + ], + "type": "string", + "description": "The type of content." + }, + "Context": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the source, e.g., a document name, database name,\r\n collection name, etc.", + "nullable": true + }, + "category": { + "type": "string", + "description": "What are the contents of the source? For example, is it a\r\n dictionary, a book chapter, business concept, a paper, etc.", + "nullable": true + }, + "path": { + "type": "string", + "description": "The path to the content, e.g., a URL, a file path, a path in a\r\n graph database, etc.", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/ContentType" + }, + "matchedContent": { + "type": "string", + "description": "The content that matched the user prompt. For text, you\r\n return the matched text and, e.g., three words before and after it.", + "nullable": true + }, + "surroundingContent": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The surrounding content of the matched content.\r\n For text, you may return, e.g., one sentence or paragraph before and after\r\n the matched content.", + "nullable": true + }, + "links": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Links to related content, e.g., links to Wikipedia articles,\r\n links to sources, etc.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Matching context returned by the data source as a result of a retrieval request." + }, + "DataSourceInfo": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the data source, e.g., \"Internal Organization Documents.\"", + "nullable": true + }, + "description": { + "type": "string", + "description": "A short description of the data source. What kind of data does it contain?\r\n What is the data source used for?", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Information about the data source." + }, + "EmbeddingInfo": { + "type": "object", + "properties": { + "embeddingType": { + "type": "string", + "description": "What kind of embedding is used. For example, \"Transformer Embedding,\" \"Contextual Word\r\n Embedding,\" \"Graph Embedding,\" etc.", + "nullable": true + }, + "embeddingName": { + "type": "string", + "description": "Name the embedding used. This can be a library, a framework, or the name of the used\r\n algorithm.", + "nullable": true + }, + "description": { + "type": "string", + "description": "A short description of the embedding. Describe what the embedding is doing.", + "nullable": true + }, + "usedWhen": { + "type": "string", + "description": "Describe when the embedding is used. For example, when the user prompt contains certain\r\n keywords, or anytime?", + "nullable": true + }, + "link": { + "type": "string", + "description": "A link to the embedding's documentation or the source code. Might be null.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Represents information about the used embedding for this data source. The purpose of this information is to give the\r\ninterested user an idea of what kind of embedding is used and what it does." + }, + "ProviderType": { + "enum": [ + "NONE", + "ANY", + "SELF_HOSTED" + ], + "type": "string", + "description": "Known types of providers that can process data." + }, + "RetrievalInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for the retrieval process. This can be a GUID, a unique name, or an increasing integer.", + "nullable": true + }, + "name": { + "type": "string", + "description": "The name of the retrieval process, e.g., \"Keyword-Based Wikipedia Article Retrieval\".", + "nullable": true + }, + "description": { + "type": "string", + "description": "A short description of the retrieval process. What kind of retrieval process is it?", + "nullable": true + }, + "link": { + "type": "string", + "description": "A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.", + "nullable": true + }, + "parametersDescription": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "A dictionary that describes the parameters of the retrieval process. The key is the parameter name,\r\n and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the\r\n expected type and range, e.g., 0.0 to 1.0 for a float parameter.", + "nullable": true + }, + "embeddings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmbeddingInfo" + }, + "description": "A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Information about a retrieval process, which this data source implements." + }, + "RetrievalRequest": { + "type": "object", + "properties": { + "latestUserPrompt": { + "type": "string", + "description": "The latest user prompt that AI Studio received.", + "nullable": true + }, + "latestUserPromptType": { + "$ref": "#/components/schemas/ContentType" + }, + "thread": { + "$ref": "#/components/schemas/ChatThread" + }, + "retrievalProcessId": { + "type": "string", + "description": "Optional. The ID of the retrieval process that the data source should use.\r\n When null, the data source chooses an appropriate retrieval process. Selecting a retrieval process is optional\r\n for AI Studio users. Most users do not specify a retrieval process.", + "nullable": true + }, + "parameters": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "A dictionary of parameters that the data source should use for the retrieval process.\r\n Although each parameter will be sent as a string, the retrieval process specifies the expected type and range.", + "nullable": true + }, + "maxMatches": { + "type": "integer", + "description": "The maximum number of matches that the data source should return. AI Studio uses\r\n any value below 1 to indicate that the data source should return as many matches as appropriate.", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "The retrieval request sent by AI Studio." + }, + "Role": { + "enum": [ + "NONE", + "UNKNOW", + "SYSTEM", + "USER", + "AI", + "AGENT" + ], + "type": "string", + "description": "Possible roles of any chat thread." + }, + "SecurityRequirements": { + "type": "object", + "properties": { + "allowedProviderType": { + "$ref": "#/components/schemas/ProviderType" + } + }, + "additionalProperties": false, + "description": "Represents the security requirements for this data source." + } + }, + "securitySchemes": { + "ERI_Token": { + "type": "apiKey", + "description": "Enter the ERI token yielded by the authentication process at /auth.", + "name": "token", + "in": "header" + } + } + }, + "security": [ + { + "ERI_Token": [ ] + } + ] +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 24c89d5..af4953b 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -9,7 +9,7 @@ authors = ["Thorsten Sommer"] tauri-build = { version = "1.5", features = [] } [dependencies] -tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open"] } +tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog"] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index e5a5bc9..e83b2b5 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -2,11 +2,13 @@ use std::sync::Mutex; use std::time::Duration; use log::{error, info, warn}; use once_cell::sync::Lazy; -use rocket::get; +use rocket::{get, post}; use rocket::serde::json::Json; use rocket::serde::Serialize; +use serde::Deserialize; use tauri::updater::UpdateResponse; use tauri::{Manager, Window}; +use tauri::api::dialog::blocking::FileDialogBuilder; use tokio::time; use crate::api_token::APIToken; use crate::dotnet::stop_dotnet_server; @@ -219,4 +221,53 @@ pub async fn install_update(_token: APIToken) { error!(Source = "Updater"; "No update available to install. Did you check for updates first?"); }, } +} + +/// Let the user select a directory. +#[post("/select/directory?", data = "<previous_directory>")] +pub fn select_directory(_token: APIToken, title: &str, previous_directory: Option<Json<PreviousDirectory>>) -> Json<DirectorySelectionResponse> { + let folder_path = match previous_directory { + Some(previous) => { + let previous_path = previous.path.as_str(); + FileDialogBuilder::new() + .set_title(title) + .set_directory(previous_path) + .pick_folder() + }, + + None => { + FileDialogBuilder::new() + .set_title(title) + .pick_folder() + }, + }; + + match folder_path { + Some(path) => { + info!("User selected directory: {path:?}"); + Json(DirectorySelectionResponse { + user_cancelled: false, + selected_directory: path.to_str().unwrap().to_string(), + }) + }, + + None => { + info!("User cancelled directory selection."); + Json(DirectorySelectionResponse { + user_cancelled: true, + selected_directory: String::from(""), + }) + }, + } +} + +#[derive(Clone, Deserialize)] +pub struct PreviousDirectory { + path: String, +} + +#[derive(Serialize)] +pub struct DirectorySelectionResponse { + user_cancelled: bool, + selected_directory: String, } \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 4c12871..963900d 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -84,6 +84,7 @@ pub fn start_runtime_api() { crate::clipboard::set_clipboard, crate::app_window::check_for_update, crate::app_window::install_update, + crate::app_window::select_directory, crate::secret::get_secret, crate::secret::store_secret, crate::secret::delete_secret,