From f521c11a606d5c6b2053ffa48e994fe904515ec9 Mon Sep 17 00:00:00 2001 From: Sabrina-devops Date: Wed, 10 Dec 2025 13:48:13 +0100 Subject: [PATCH] Added the possibility to attach files to the chat (#585) Co-authored-by: Thorsten Sommer --- .../DocumentAnalysisAssistant.razor | 2 +- .../Assistants/I18N/allTexts.lua | 6 ++ .../Chat/ContentBlockComponent.razor | 10 ++- app/MindWork AI Studio/Chat/ContentImage.cs | 3 + app/MindWork AI Studio/Chat/ContentText.cs | 39 +++++++++ app/MindWork AI Studio/Chat/IContent.cs | 7 ++ .../Chat/ListContentBlockExtensions.cs | 25 ++++++ .../Components/AttachDocuments.razor | 80 +++++++++++------ .../Components/AttachDocuments.razor.cs | 5 ++ .../Components/ChatComponent.razor | 5 ++ .../Components/ChatComponent.razor.cs | 14 ++- .../Components/ReadFileContent.razor.cs | 8 ++ .../Dialogs/Settings/SettingsDialogBase.cs | 2 +- .../SettingsDialogChatTemplate.razor.cs | 2 +- .../plugin.lua | 7 ++ .../plugin.lua | 7 ++ app/MindWork AI Studio/Program.cs | 1 + .../AlibabaCloud/ProviderAlibabaCloud.cs | 40 +++++---- .../Provider/Anthropic/ProviderAnthropic.cs | 37 ++++---- .../Provider/DeepSeek/ProviderDeepSeek.cs | 40 +++++---- .../Provider/Fireworks/ProviderFireworks.cs | 39 +++++---- .../Provider/GWDG/ProviderGWDG.cs | 40 +++++---- .../Provider/Google/ProviderGoogle.cs | 39 +++++---- .../Provider/Groq/ProviderGroq.cs | 39 +++++---- .../Provider/Helmholtz/ProviderHelmholtz.cs | 40 +++++---- .../HuggingFace/ProviderHuggingFace.cs | 40 +++++---- .../Provider/Mistral/ProviderMistral.cs | 40 +++++---- .../Provider/OpenAI/ProviderOpenAI.cs | 39 +++++---- .../Provider/Perplexity/ProviderPerplexity.cs | 39 +++++---- .../Provider/SelfHosted/ProviderSelfHosted.cs | 39 +++++---- .../Provider/X/ProviderX.cs | 39 +++++---- .../Services/PandocAvailabilityService.cs | 85 +++++++++++++++++++ .../wwwroot/changelog/v0.9.55.md | 1 + 33 files changed, 579 insertions(+), 280 deletions(-) create mode 100644 app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs create mode 100644 app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor index 841cf0b2..0cbdc441 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor @@ -103,7 +103,7 @@ else @T("Documents for the analysis") - + diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 431277f2..42b438ca 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1468,6 +1468,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message" +-- Number of attachments +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments" + -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet." @@ -1504,6 +1507,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T2928927510"] = "Videos -- Images are not supported yet UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T298062956"] = "Images are not supported yet" +-- Click to attach files +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T3521845090"] = "Click to attach files" + -- Clear file list UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T3759696136"] = "Clear file list" diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 164b07c0..90f889bd 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -16,11 +16,19 @@ + @if (this.Content.FileAttachments.Count > 0) + { + + + + + + } @if (this.Content.Sources.Count > 0) { - + } diff --git a/app/MindWork AI Studio/Chat/ContentImage.cs b/app/MindWork AI Studio/Chat/ContentImage.cs index 467d3e5f..6026e554 100644 --- a/app/MindWork AI Studio/Chat/ContentImage.cs +++ b/app/MindWork AI Studio/Chat/ContentImage.cs @@ -30,6 +30,9 @@ public sealed class ContentImage : IContent, IImageSource /// public List Sources { get; set; } = []; + /// + public List FileAttachments { get; set; } = []; + /// public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatChatThread, CancellationToken token = default) { diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index ef47f808..09a4e0af 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Text.Json.Serialization; using AIStudio.Provider; @@ -39,6 +40,9 @@ public sealed class ContentText : IContent /// public List Sources { get; set; } = []; + + /// + public List FileAttachments { get; set; } = []; /// public async Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default) @@ -139,9 +143,44 @@ public sealed class ContentText : IContent Text = this.Text, InitialRemoteWait = this.InitialRemoteWait, IsStreaming = this.IsStreaming, + Sources = [..this.Sources], + FileAttachments = [..this.FileAttachments], }; #endregion + + public async Task PrepareContentForAI() + { + var sb = new StringBuilder(); + sb.AppendLine(this.Text); + + if(this.FileAttachments.Count > 0) + { + // Check Pandoc availability once before processing file attachments + var pandocState = await Pandoc.CheckAvailabilityAsync(Program.RUST_SERVICE, showMessages: true, showSuccessMessage: false); + + if (!pandocState.IsAvailable) + LOGGER.LogWarning("File attachments could not be processed because Pandoc is not available."); + else if (!pandocState.CheckWasSuccessful) + LOGGER.LogWarning("File attachments could not be processed because the Pandoc version check failed."); + else + { + sb.AppendLine(); + sb.AppendLine("The following files are attached to this message:"); + foreach(var file in this.FileAttachments) + { + sb.AppendLine(); + sb.AppendLine("---------------------------------------"); + sb.AppendLine($"File path: {file}"); + sb.AppendLine("File content:"); + sb.AppendLine("````"); + sb.AppendLine(await Program.RUST_SERVICE.ReadArbitraryFileData(file, int.MaxValue)); + sb.AppendLine("````"); + } + } + } + return sb.ToString(); + } /// /// The text content. diff --git a/app/MindWork AI Studio/Chat/IContent.cs b/app/MindWork AI Studio/Chat/IContent.cs index 2400e14e..883a1b6c 100644 --- a/app/MindWork AI Studio/Chat/IContent.cs +++ b/app/MindWork AI Studio/Chat/IContent.cs @@ -47,6 +47,13 @@ public interface IContent /// [JsonIgnore] public List Sources { get; set; } + + /// + /// Represents a collection of file attachments associated with the content. + /// This property contains a list of file paths that are appended + /// to the content to provide additional context or resources. + /// + public List FileAttachments { get; set; } /// /// Uses the provider to create the content. diff --git a/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs b/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs new file mode 100644 index 00000000..f5e8c0ab --- /dev/null +++ b/app/MindWork AI Studio/Chat/ListContentBlockExtensions.cs @@ -0,0 +1,25 @@ +namespace AIStudio.Chat; + +public static class ListContentBlockExtensions +{ + /// + /// Processes a list of content blocks by transforming them into a collection of message results asynchronously. + /// + /// The list of content blocks to process. + /// A function that transforms each content block into a message result asynchronously. + /// The type of the result produced by the transformation function. + /// An asynchronous task that resolves to a list of transformed results. + public static async Task> BuildMessages(this List blocks, Func> transformer) + { + var messages = blocks + .Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)) + .Select(transformer) + .ToList(); + + // Await all messages: + await Task.WhenAll(messages); + + // Select all results: + return messages.Select(n => n.Result).ToList(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor b/app/MindWork AI Studio/Components/AttachDocuments.razor index cc1899d3..37e5deea 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor @@ -1,27 +1,59 @@ @inherits MSGComponentBase - - - @T("Drag and drop files into the marked area or click here to attach documents: ") - - @T("Add file") - - -
- - @foreach (var fileInfo in this.DocumentPaths.Select(file => new FileInfo(file))) - { - +@if (this.UseSmallForm) +{ +
+ @{ + var fileInfos = this.DocumentPaths.Select(file => new FileInfo(file)).ToList(); } - -
- - @T("Clear file list") - \ No newline at end of file + @if (fileInfos.Any()) + { + + + + } + else + { + + + + } +
+} +else +{ + + + @T("Drag and drop files into the marked area or click here to attach documents: ") + + + @T("Add file") + + +
+ + @foreach (var fileInfo in this.DocumentPaths.Select(file => new FileInfo(file))) + { + + } + +
+ + @T("Clear file list") + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs index 0e1dc919..7d5aab76 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs @@ -28,6 +28,9 @@ public partial class AttachDocuments : MSGComponentBase [Parameter] public bool CatchAllDocuments { get; set; } + [Parameter] + public bool UseSmallForm { get; set; } + [Inject] private ILogger Logger { get; set; } = null!; @@ -37,6 +40,8 @@ public partial class AttachDocuments : MSGComponentBase [Inject] private IDialogService DialogService { get; init; } = null!; + private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; + #region Overrides of MSGComponentBase protected override async Task OnInitializedAsync() diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 48420522..6a8e4ad9 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -82,6 +82,11 @@ } + + @if (this.isPandocAvailable) + { + + } @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) { diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs index 94ee2385..502e6966 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs +++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs @@ -3,6 +3,7 @@ using AIStudio.Dialogs; using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Settings.DataModel; +using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -33,9 +34,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable [Inject] private ILogger Logger { get; set; } = null!; - + [Inject] private IDialogService DialogService { get; init; } = null!; + + [Inject] + private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); @@ -57,6 +61,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable private string currentWorkspaceName = string.Empty; private Guid currentWorkspaceId = Guid.Empty; private CancellationTokenSource? cancellationTokenSource; + private HashSet chatDocumentPaths = []; + private bool isPandocAvailable; // Unfortunately, we need the input field reference to blur the focus away. Without // this, we cannot clear the input field. @@ -197,6 +203,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Select the correct provider: await this.SelectProviderWhenLoadingChat(); + + // Check if Pandoc is available (no dialog or messages): + this.isPandocAvailable = await this.PandocAvailabilityService.IsAvailableAsync(); + await base.OnInitializedAsync(); } @@ -462,6 +472,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable lastUserPrompt = new ContentText { Text = this.userInput, + FileAttachments = this.chatDocumentPaths.ToList(), }; // @@ -507,6 +518,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable // Clear the input field: await this.inputField.FocusAsync(); this.userInput = string.Empty; + this.chatDocumentPaths.Clear(); await this.inputField.BlurAsync(); // Enable the stream state for the chat component: diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs index d09bea4b..e40d8088 100644 --- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs +++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs @@ -24,6 +24,9 @@ public partial class ReadFileContent : MSGComponentBase [Inject] private ILogger Logger { get; init; } = null!; + + [Inject] + private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!; private async Task SelectFile() { @@ -62,6 +65,11 @@ public partial class ReadFileContent : MSGComponentBase return; } + // Ensure that Pandoc is installed and ready: + await this.PandocAvailabilityService.EnsureAvailabilityAsync( + showSuccessMessage: false, + showDialog: true); + try { var fileContent = await UserFile.LoadFileData(selectedFile.SelectedFilePath, this.RustService, this.DialogService); diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs index 1dd94c1c..0dd1af1b 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBase.cs @@ -23,7 +23,7 @@ public abstract class SettingsDialogBase : MSGComponentBase protected readonly List> availableEmbeddingProviders = new(); #region Overrides of ComponentBase - + /// protected override async Task OnInitializedAsync() { diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs index 93084866..2708ab4f 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs @@ -13,7 +13,7 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase public ChatThread? ExistingChatThread { get; set; } #region Overrides of ComponentBase - + /// protected override async Task OnInitializedAsync() { diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index e97c1721..03e06004 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1470,6 +1470,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Entfern -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Nachricht neu erstellen" +-- Number of attachments +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Anzahl der Anhänge" + -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Der Inhaltstyp {0} kann noch nicht angezeigt werden." @@ -1506,6 +1509,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T2928927510"] = "Videos -- Images are not supported yet UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T298062956"] = "Bilder werden noch nicht unterstützt." +-- Click to attach files +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T3521845090"] = "Klicken, um Dateien anzuhängen" + -- Clear file list UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T3759696136"] = "Dateiliste löschen" @@ -6032,3 +6038,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen" + diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 2462eb98..632e1c87 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1470,6 +1470,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2093355991"] = "Removes -- Regenerate Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2308444540"] = "Regenerate Message" +-- Number of attachments +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments" + -- Cannot render content of type {0} yet. UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet." @@ -1506,6 +1509,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T2928927510"] = "Videos -- Images are not supported yet UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T298062956"] = "Images are not supported yet" +-- Click to attach files +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T3521845090"] = "Click to attach files" + -- Clear file list UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T3759696136"] = "Clear file list" @@ -6032,3 +6038,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" + diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 2514a67f..b5954efc 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -126,6 +126,7 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index e7d3e523..78618db2 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -39,6 +39,26 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the AlibabaCloud HTTP chat request: var alibabaCloudChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest { @@ -47,24 +67,8 @@ public sealed class ProviderAlibabaCloud() : BaseProvider("https://dashscope-int // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], + Stream = true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index f2c88f52..4ea73e77 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -30,29 +30,32 @@ public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.co // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters("system"); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the Anthropic HTTP chat request: var chatRequest = JsonSerializer.Serialize(new ChatRequest { Model = chatModel.Id, // Build the messages: - Messages = [..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [..messages], System = chatThread.PrepareSystemPrompt(settingsManager, chatThread), MaxTokens = apiParameters.TryGetValue("max_tokens", out var value) && value is int intValue ? intValue : 4_096, diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index 1063262c..991d6a2e 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -39,6 +39,26 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/ // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the DeepSeek HTTP chat request: var deepSeekChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest { @@ -47,24 +67,8 @@ public sealed class ProviderDeepSeek() : BaseProvider("https://api.deepseek.com/ // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], + Stream = true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index a02c692c..20c79188 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -39,6 +39,26 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the Fireworks HTTP chat request: var fireworksChatRequest = JsonSerializer.Serialize(new ChatRequest { @@ -47,24 +67,7 @@ public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/infere // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 2a56bfd4..b1cb291c 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -39,6 +39,26 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the GWDG HTTP chat request: var gwdgChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest { @@ -47,24 +67,8 @@ public sealed class ProviderGWDG() : BaseProvider("https://chat-ai.academiccloud // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], + Stream = true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index 8dcf0c96..7ce3f24e 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -39,6 +39,26 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the Google HTTP chat request: var geminiChatRequest = JsonSerializer.Serialize(new ChatRequest { @@ -47,24 +67,7 @@ public class ProviderGoogle() : BaseProvider("https://generativelanguage.googlea // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 5cc7b3df..45473d82 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -39,6 +39,26 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the OpenAI HTTP chat request: var groqChatRequest = JsonSerializer.Serialize(new ChatRequest { @@ -47,24 +67,7 @@ public class ProviderGroq() : BaseProvider("https://api.groq.com/openai/v1/", LO // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index f0b69bb4..3f7b405b 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -38,6 +38,26 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); // Prepare the Helmholtz HTTP chat request: var helmholtzChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest @@ -47,24 +67,8 @@ public sealed class ProviderHelmholtz() : BaseProvider("https://api.helmholtz-bl // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], + Stream = true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 6cfb8027..31522b5c 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -44,6 +44,26 @@ public sealed class ProviderHuggingFace : BaseProvider // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var message = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the HuggingFace HTTP chat request: var huggingfaceChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest { @@ -52,24 +72,8 @@ public sealed class ProviderHuggingFace : BaseProvider // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..message], + Stream = true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 01b0db11..bd999b92 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -36,6 +36,26 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/ // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new RegularMessage + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); // Prepare the Mistral HTTP chat request: var mistralChatRequest = JsonSerializer.Serialize(new ChatRequest @@ -45,24 +65,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/ // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new RegularMessage - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, @@ -70,6 +73,7 @@ public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/ AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); + async Task RequestBuilder() { // Build the HTTP post request: diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index be38cedc..89da7b7d 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -88,6 +88,26 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/" // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters("input", "store", "tools"); + + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => systemPromptRole, + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); // // Create the request: either for the Responses API or the Chat Completion API @@ -102,24 +122,7 @@ public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/" // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => systemPromptRole, - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index a15a7e3a..3687ad7b 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -48,6 +48,26 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity. // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message() + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the Perplexity HTTP chat request: var perplexityChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest { @@ -56,24 +76,7 @@ public sealed class ProviderPerplexity() : BaseProvider("https://api.perplexity. // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], Stream = true, AdditionalApiParameters = apiParameters }, JSON_SERIALIZER_OPTIONS); diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index abb15532..4389099e 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -35,6 +35,26 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the OpenAI HTTP chat request: var providerChatRequest = JsonSerializer.Serialize(new ChatRequest { @@ -43,24 +63,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index b1743c53..e8a0b2e7 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -39,6 +39,26 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER) // Parse the API parameters: var apiParameters = this.ParseAdditionalApiParameters(); + // Build the list of messages: + var messages = await chatThread.Blocks.BuildMessages(async n => new Message() + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => await text.PrepareContentForAI(), + _ => string.Empty, + } + }); + // Prepare the xAI HTTP chat request: var xChatRequest = JsonSerializer.Serialize(new ChatCompletionAPIRequest { @@ -47,24 +67,7 @@ public sealed class ProviderX() : BaseProvider("https://api.x.ai/v1/", LOGGER) // Build the messages: // - First of all the system prompt // - Then none-empty user and AI messages - Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message - { - Role = n.Role switch - { - ChatRole.USER => "user", - ChatRole.AI => "assistant", - ChatRole.AGENT => "assistant", - ChatRole.SYSTEM => "system", - - _ => "user", - }, - - Content = n.Content switch - { - ContentText text => text.Text, - _ => string.Empty, - } - }).ToList()], + Messages = [systemPrompt, ..messages], // Right now, we only support streaming completions: Stream = true, diff --git a/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs new file mode 100644 index 00000000..14a26908 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs @@ -0,0 +1,85 @@ +using AIStudio.Dialogs; +using AIStudio.Tools.PluginSystem; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Tools.Services; + +/// +/// Service to check Pandoc availability and ensure installation. +/// This service encapsulates the logic for checking if Pandoc is installed +/// and showing the installation dialog if needed. +/// +public sealed class PandocAvailabilityService(RustService rustService, IDialogService dialogService, ILogger logger) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PandocAvailabilityService).Namespace, nameof(PandocAvailabilityService)); + + private RustService RustService => rustService; + + private IDialogService DialogService => dialogService; + + private ILogger Logger => logger; + + private PandocInstallation? cachedInstallation; + + /// + /// Checks if Pandoc is available and shows the installation dialog if needed. + /// + /// Whether to show a success message if Pandoc is available. + /// Whether to show the installation dialog if Pandoc is not available. + /// The Pandoc installation state. + public async Task EnsureAvailabilityAsync(bool showSuccessMessage = false, bool showDialog = true) + { + // Check if Pandoc is available: + var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: false, showSuccessMessage: showSuccessMessage); + + // Cache the result: + this.cachedInstallation = pandocState; + + // If not available, show installation dialog: + if (!pandocState.IsAvailable && showDialog) + { + var dialogParameters = new DialogParameters + { + { x => x.ShowInitialResultInSnackbar, false }, + }; + + var dialogReference = await this.DialogService.ShowAsync(TB("Pandoc Installation"), dialogParameters, DialogOptions.FULLSCREEN); + await dialogReference.Result; + + // Re-check availability after dialog: + pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: showSuccessMessage, showSuccessMessage: showSuccessMessage); + this.cachedInstallation = pandocState; + + if (!pandocState.IsAvailable) + { + this.Logger.LogError("Pandoc is not available after installation attempt."); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc may be required for importing files."))); + } + } + + return pandocState; + } + + /// + /// Checks if Pandoc is available without showing any dialogs or messages. + /// Uses cached result if available to avoid redundant checks. + /// + /// True if Pandoc is available, false otherwise. + public async Task IsAvailableAsync() + { + if (this.cachedInstallation.HasValue) + return this.cachedInstallation.Value.IsAvailable; + + var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: false, showSuccessMessage: false); + this.cachedInstallation = pandocState; + + return pandocState.IsAvailable; + } + + /// + /// Clears the cached Pandoc installation state. + /// Useful when the installation state might have changed. + /// + public void ClearCache() => this.cachedInstallation = null; +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md index b3d95d68..cc056442 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md @@ -1,6 +1,7 @@ # v0.9.55, build 230 (2025-12-xx xx:xx UTC) - Added support for newer Mistral models (Mistral 3, Voxtral, and Magistral). - Added a description field to local data sources (preview feature) so that the data selection agent has more information about which data each local source contains when selecting data sources. +- Added the ability to use file attachments in chat. This is the initial implementation of this feature. We will continue to develop this feature and refine it further based on user feedback. Many thanks to Sabrina `Sabrina-devops` for this wonderful contribution. - Improved the document analysis assistant (in preview) by adding descriptions to the different sections. - Improved the document preview dialog for the document analysis assistant (in preview), providing Markdown and plain text views for attached files. - Improved the ID handling for configuration plugins.