From f5cb574be8664bbc35ea48bc007d3a38972f9834 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 14 Jul 2024 18:45:53 +0200 Subject: [PATCH] Refactor basic assistant concepts into a base component --- .../Components/AssistantBase.razor | 39 ++++++ .../Components/AssistantBase.razor.cs | 104 +++++++++++++++ .../Components/AssistantBaseCore.cs | 19 +++ .../IconFinder/AssistantIconFinder.razor | 83 ++++-------- .../IconFinder/AssistantIconFinder.razor.cs | 119 ++++-------------- 5 files changed, 210 insertions(+), 154 deletions(-) create mode 100644 app/MindWork AI Studio/Components/AssistantBase.razor create mode 100644 app/MindWork AI Studio/Components/AssistantBase.razor.cs create mode 100644 app/MindWork AI Studio/Components/AssistantBaseCore.cs diff --git a/app/MindWork AI Studio/Components/AssistantBase.razor b/app/MindWork AI Studio/Components/AssistantBase.razor new file mode 100644 index 00000000..e04bb340 --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantBase.razor @@ -0,0 +1,39 @@ +@using AIStudio.Chat + + @this.Title + + + + + + + @this.Description + + + @if (this.Body is not null) + { + @this.Body + } + + + @if (this.inputIssues.Any()) + { + + Issues + + @foreach (var issue in this.inputIssues) + { + + @issue + + } + + + } + + @if (this.resultingContentBlock is not null) + { + + } + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantBase.razor.cs b/app/MindWork AI Studio/Components/AssistantBase.razor.cs new file mode 100644 index 00000000..a15afe4a --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantBase.razor.cs @@ -0,0 +1,104 @@ +using AIStudio.Chat; +using AIStudio.Provider; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public abstract partial class AssistantBase : ComponentBase +{ + [Inject] + protected SettingsManager SettingsManager { get; set; } = null!; + + [Inject] + protected IJSRuntime JsRuntime { get; init; } = null!; + + [Inject] + protected Random RNG { get; set; } = null!; + + protected string Title { get; init; } = string.Empty; + + protected string Description { get; init; } = string.Empty; + + protected abstract string SystemPrompt { get; } + + private protected virtual RenderFragment? Body => null; + + protected AIStudio.Settings.Provider selectedProvider; + protected MudForm? form; + protected bool inputIsValid; + + private ChatThread? chatThread; + private ContentBlock? resultingContentBlock; + private string[] inputIssues = []; + + #region Overrides of ComponentBase + + 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(firstRender) + this.form?.ResetValidation(); + + await base.OnAfterRenderAsync(firstRender); + } + + #endregion + + protected void CreateChatThread() + { + this.chatThread = new() + { + WorkspaceId = Guid.Empty, + ChatId = Guid.NewGuid(), + Name = string.Empty, + Seed = this.RNG.Next(), + SystemPrompt = this.SystemPrompt, + Blocks = [], + }; + } + + protected DateTimeOffset AddUserRequest(string request) + { + var time = DateTimeOffset.Now; + this.chatThread!.Blocks.Add(new ContentBlock + { + Time = time, + ContentType = ContentType.TEXT, + Role = ChatRole.USER, + Content = new ContentText + { + Text = request, + }, + }); + + return time; + } + + protected async Task AddAIResponseAsync(DateTimeOffset time) + { + var aiText = new ContentText + { + // We have to wait for the remote + // for the content stream: + InitialRemoteWait = true, + }; + + this.resultingContentBlock = new ContentBlock + { + Time = time, + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = aiText, + }; + + this.chatThread?.Blocks.Add(this.resultingContentBlock); + + // Use the selected provider to get the AI response. + // By awaiting this line, we wait for the entire + // content to be streamed. + await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AssistantBaseCore.cs b/app/MindWork AI Studio/Components/AssistantBaseCore.cs new file mode 100644 index 00000000..5508b14f --- /dev/null +++ b/app/MindWork AI Studio/Components/AssistantBaseCore.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace AIStudio.Components; + +// +// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed +// + +public abstract class AssistantBaseCore : AssistantBase +{ + private protected sealed override RenderFragment Body => this.BuildRenderTree; + + // Allow content to be provided by a .razor file but without + // overriding the content of the base class + protected new virtual void BuildRenderTree(RenderTreeBuilder builder) + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor index a7b8132d..1e5e214d 100644 --- a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor +++ b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor @@ -1,68 +1,29 @@ @page "/assistant/icons" -@using AIStudio.Chat @using AIStudio.Settings +@inherits AssistantBaseCore - - Icon Finder - + - - - - - Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge: - You need to extract a concept from your context, such as from a text. Let's take an example where - your text contains statements about multiple departments. The sought-after concept could be "departments." - The next challenge is that we need to anticipate the bias of the icon designers: under the search term - "departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source, - it might be more effective to search for "buildings," for instance. LLMs assist you with both steps. - - - - - - - @foreach (var source in Enum.GetValues()) - { - @source.Name() - } - - @if (this.selectedIconSource is not IconSources.GENERIC) - { - Open website - } - - - - @foreach (var provider in this.SettingsManager.ConfigurationData.Providers) - { - - } - - - - Find icons - - - - @if (this.inputIssues.Any()) + + + @foreach (var source in Enum.GetValues()) { - - Issues - - @foreach (var issue in this.inputIssues) - { - - @issue - - } - - + @source.Name() } + + @if (this.selectedIconSource is not IconSources.GENERIC) + { + Open website + } + - @if (this.resultingContentBlock is not null) - { - - } - - \ No newline at end of file + + @foreach (var provider in this.SettingsManager.ConfigurationData.Providers) + { + + } + + + + Find icon + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs index 4f173f7e..5443cf80 100644 --- a/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs +++ b/app/MindWork AI Studio/Components/Pages/IconFinder/AssistantIconFinder.razor.cs @@ -1,45 +1,26 @@ -using AIStudio.Chat; using AIStudio.Provider; -using AIStudio.Settings; - -using Microsoft.AspNetCore.Components; namespace AIStudio.Components.Pages.IconFinder; -public partial class AssistantIconFinder : ComponentBase +public partial class AssistantIconFinder : AssistantBaseCore { - [Inject] - private SettingsManager SettingsManager { get; set; } = null!; - - [Inject] - public IJSRuntime JsRuntime { get; init; } = null!; - - [Inject] - public Random RNG { get; set; } = null!; - - private ChatThread? chatThread; - private ContentBlock? resultingContentBlock; - private AIStudio.Settings.Provider selectedProvider; - private MudForm form = null!; - private bool inputIsValid; - private string[] inputIssues = []; private string inputContext = string.Empty; private IconSources selectedIconSource; - - #region Overrides of ComponentBase - - protected override async Task OnAfterRenderAsync(bool firstRender) + + public AssistantIconFinder() { - // 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(firstRender) - this.form.ResetValidation(); - - await base.OnAfterRenderAsync(firstRender); + this.Title = "Icon Finder"; + this.Description = + """ + Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge: + You need to extract a concept from your context, such as from a text. Let's take an example where + your text contains statements about multiple departments. The sought-after concept could be "departments." + The next challenge is that we need to anticipate the bias of the icon designers: under the search term + "departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source, + it might be more effective to search for "buildings," for instance. LLMs assist you with both steps. + """; } - - #endregion - + private string? ValidatingContext(string context) { if(string.IsNullOrWhiteSpace(context)) @@ -58,72 +39,24 @@ public partial class AssistantIconFinder : ComponentBase private async Task FindIcon() { - await this.form.Validate(); + await this.form!.Validate(); if (!this.inputIsValid) return; - // - // Create a new chat thread: - // - this.chatThread = new() - { - WorkspaceId = Guid.Empty, - ChatId = Guid.NewGuid(), - Name = string.Empty, - Seed = this.RNG.Next(), - SystemPrompt = SYSTEM_PROMPT, - Blocks = [], - }; - - // - // Add the user's request to the thread: - // - var time = DateTimeOffset.Now; - this.chatThread.Blocks.Add(new ContentBlock - { - Time = time, - ContentType = ContentType.TEXT, - Role = ChatRole.USER, - Content = new ContentText - { - Text = - $""" - {this.selectedIconSource.Prompt()} I search for an icon for the following context: - - ``` - {this.inputContext} - ``` - """, - }, - }); - - // - // Add the AI response to the thread: - // - var aiText = new ContentText - { - // We have to wait for the remote - // for the content stream: - InitialRemoteWait = true, - }; + this.CreateChatThread(); + var time = this.AddUserRequest( + $""" + {this.selectedIconSource.Prompt()} I search for an icon for the following context: + + ``` + {this.inputContext} + ``` + """); - this.resultingContentBlock = new ContentBlock - { - Time = time, - ContentType = ContentType.TEXT, - Role = ChatRole.AI, - Content = aiText, - }; - - this.chatThread?.Blocks.Add(this.resultingContentBlock); - - // Use the selected provider to get the AI response. - // By awaiting this line, we wait for the entire - // content to be streamed. - await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread); + await this.AddAIResponseAsync(time); } - private const string SYSTEM_PROMPT = + protected override string SystemPrompt => """ I can search for icons using US English keywords. Please help me come up with the right search queries. I don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords