From 2cc27859ce2ee2a952faa6fe3c6cf8cd280ba0dc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 10 Aug 2026 09:09:37 +0200 Subject: [PATCH] Fixed PDFs and other formats requiring Pandoc although they do not use it --- app/MindWork AI Studio/Chat/ContentText.cs | 129 ++++++++++-------- .../Components/AttachDocuments.razor.cs | 20 ++- .../Tools/Rust/FileTypes.cs | 17 +++ 3 files changed, 103 insertions(+), 63 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs index 906c0ee1..7ffbab88 100644 --- a/app/MindWork AI Studio/Chat/ContentText.cs +++ b/app/MindWork AI Studio/Chat/ContentText.cs @@ -5,6 +5,7 @@ using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.RAG.RAGProcesses; +using AIStudio.Tools.Rust; namespace AIStudio.Chat; @@ -282,70 +283,84 @@ public sealed class ContentText : IContent // Only proceed if there are existing, allowed documents: if (existingDocuments.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 + // + // Pandoc is only needed for the few formats we convert with it. PDFs, text files, + // spreadsheets, and presentations are read by the runtime itself, so a missing + // Pandoc installation must not stop them. + // + var pandocIsUsable = true; + if (existingDocuments.Any(document => FileTypes.RequiresPandoc(document.FilePath))) { + var pandocState = await Pandoc.CheckAvailabilityAsync(Program.RUST_SERVICE, showMessages: true, showSuccessMessage: false); + pandocIsUsable = pandocState is { IsAvailable: true, CheckWasSuccessful: true }; + + if (!pandocState.IsAvailable) + LOGGER.LogWarning("File attachments which need Pandoc could not be processed because Pandoc is not available."); + else if (!pandocState.CheckWasSuccessful) + LOGGER.LogWarning("File attachments which need Pandoc could not be processed because the Pandoc version check failed."); + } + + // + // The document blocks are collected separately, so we only announce attached + // files when at least one of them could actually be read. Announcing files we + // then hand over as empty blocks makes the AI answer about an empty document. + // + var documentBlocks = new StringBuilder(); + foreach(var document in existingDocuments) + { + if (document.IsForbidden) + { + LOGGER.LogWarning("File attachment '{FilePath}' has a forbidden file type and will be skipped.", document.FilePath); + continue; + } + + if (!pandocIsUsable && FileTypes.RequiresPandoc(document.FilePath)) + { + LOGGER.LogWarning("The file attachment '{FilePath}' needs Pandoc and will be skipped.", document.FilePath); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, string.Format(TB("The file '{0}' needs Pandoc to be read and was not sent."), document.FileName))); + continue; + } + + var extraction = await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue); + if (!extraction.HasUsableContent) + { + LOGGER.LogError("Reading the file attachment '{FilePath}' failed and it will not be sent: code={ErrorCode}, message='{ErrorMessage}'.", document.FilePath, extraction.ErrorCode, extraction.ErrorMessage); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, extraction.ToUserMessage(document.FileName))); + continue; + } + // - // The document blocks are collected separately, so we only announce attached - // files when at least one of them could actually be read. Announcing files we - // then hand over as empty blocks makes the AI answer about an empty document. + // The file is usable, but we lost parts of it. The user has to know which + // parts are missing, because the answer will be based on the rest. // - var documentBlocks = new StringBuilder(); - foreach(var document in existingDocuments) + if (extraction.Outcome is FileExtractionOutcome.PARTIAL) { - if (document.IsForbidden) - { - LOGGER.LogWarning("File attachment '{FilePath}' has a forbidden file type and will be skipped.", document.FilePath); - continue; - } - - var extraction = await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue); - if (!extraction.HasUsableContent) - { - LOGGER.LogError("Reading the file attachment '{FilePath}' failed and it will not be sent: code={ErrorCode}, message='{ErrorMessage}'.", document.FilePath, extraction.ErrorCode, extraction.ErrorMessage); - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, extraction.ToUserMessage(document.FileName))); - continue; - } - - // - // The file is usable, but we lost parts of it. The user has to know which - // parts are missing, because the answer will be based on the rest. - // - if (extraction.Outcome is FileExtractionOutcome.PARTIAL) - { - LOGGER.LogWarning("Parts of the file attachment '{FilePath}' could not be read: pages={FailedPages}.", document.FilePath, string.Join(", ", extraction.FailedPages)); - await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(document.FileName))); - } - - documentBlocks.AppendLine(); - documentBlocks.AppendLine("---------------------------------------"); - documentBlocks.AppendLine($"File path: {document.FilePath}"); - documentBlocks.AppendLine("File content:"); - documentBlocks.AppendLine("````"); - documentBlocks.AppendLine(extraction.Content); - documentBlocks.AppendLine("````"); + LOGGER.LogWarning("Parts of the file attachment '{FilePath}' could not be read: pages={FailedPages}.", document.FilePath, string.Join(", ", extraction.FailedPages)); + await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(document.FileName))); } - if (documentBlocks.Length > 0) - { - sb.AppendLine(); - sb.AppendLine("The following files are attached to this message:"); - sb.Append(documentBlocks); - } + documentBlocks.AppendLine(); + documentBlocks.AppendLine("---------------------------------------"); + documentBlocks.AppendLine($"File path: {document.FilePath}"); + documentBlocks.AppendLine("File content:"); + documentBlocks.AppendLine("````"); + documentBlocks.AppendLine(extraction.Content); + documentBlocks.AppendLine("````"); + } - var numImages = normalizedAttachments.Count(x => x is { IsImage: true, Exists: true }); - if (numImages > 0) - { - sb.AppendLine(); - sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. "); - sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly."); - } + if (documentBlocks.Length > 0) + { + sb.AppendLine(); + sb.AppendLine("The following files are attached to this message:"); + sb.Append(documentBlocks); + } + + var numImages = normalizedAttachments.Count(x => x is { IsImage: true, Exists: true }); + if (numImages > 0) + { + sb.AppendLine(); + sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. "); + sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly."); } } } diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs index 4b4274fd..9849d102 100644 --- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs +++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs @@ -443,23 +443,31 @@ public partial class AttachDocuments : MSGComponentBase var mediaPaths = existingPaths.Where(IsTranscribableMedia).ToList(); var regularPaths = existingPaths.Except(mediaPaths).ToList(); - var canAddRegularFiles = true; - if (regularPaths.Count > 0) + // + // Only the formats we convert with Pandoc depend on a Pandoc installation. Everything + // else, PDFs in particular, is read by the Rust runtime itself, so those files must stay + // attachable without Pandoc. + // + var canAddPandocFiles = true; + if (regularPaths.Any(FileTypes.RequiresPandoc)) { var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync( showSuccessMessage: false, showDialog: true); - canAddRegularFiles = pandocState.IsAvailable; + canAddPandocFiles = pandocState.IsAvailable; } foreach (var path in regularPaths) { - if (!canAddRegularFiles) - break; + if (!canAddPandocFiles && FileTypes.RequiresPandoc(path)) + { + this.Logger.LogWarning("The file '{Path}' needs Pandoc and was not attached.", path); + continue; + } if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider)) continue; - + this.DocumentPaths.Add(FileAttachment.FromPath(path)); } diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 038833f0..51cc0c1e 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -84,6 +84,23 @@ public static class FileTypes public static readonly FileTypeFilter EXECUTABLES = FileTypeFilter.Leaf(TB("Executable"), "exe", "app", "bin", "appimage"); public static readonly FileTypeFilter PLUGIN_ARCHIVE = FileTypeFilter.Leaf(TB("Plugin archive"), PluginArchive.PLUGIN_FILE_EXTENSION.TrimStart('.'), "zip"); + /// + /// The file types AI Studio converts using Pandoc. + /// + /// + /// This is not a user-selectable type, it mirrors the formats the Rust runtime hands to + /// Pandoc. Every other document type is read by the runtime itself, so it must never depend + /// on a Pandoc installation. The name is not localized because it is never shown. + /// + private static readonly FileTypeFilter PANDOC_CONVERTED = FileTypeFilter.Leaf("Pandoc conversion", "docx", "odt", "html", "htm"); + + /// + /// Determines whether reading the given file needs Pandoc. + /// + /// The path of the file to check. + /// True, when reading the file needs Pandoc. + public static bool RequiresPandoc(string filePath) => IsAllowedPath(filePath, PANDOC_CONVERTED); + public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types) { if (types == null || types.Length == 0)