Fixed PDFs and other formats requiring Pandoc although they do not use it

This commit is contained in:
Thorsten Sommer 2026-08-10 09:09:37 +02:00
parent cf4661d1ad
commit 2cc27859ce
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
3 changed files with 103 additions and 63 deletions

View File

@ -5,6 +5,7 @@ using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.RAG.RAGProcesses; using AIStudio.Tools.RAG.RAGProcesses;
using AIStudio.Tools.Rust;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@ -282,70 +283,84 @@ public sealed class ContentText : IContent
// Only proceed if there are existing, allowed documents: // Only proceed if there are existing, allowed documents:
if (existingDocuments.Count > 0) if (existingDocuments.Count > 0)
{ {
// Check Pandoc availability once before processing file attachments //
var pandocState = await Pandoc.CheckAvailabilityAsync(Program.RUST_SERVICE, showMessages: true, showSuccessMessage: false); // 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
if (!pandocState.IsAvailable) // Pandoc installation must not stop them.
LOGGER.LogWarning("File attachments could not be processed because Pandoc is not available."); //
else if (!pandocState.CheckWasSuccessful) var pandocIsUsable = true;
LOGGER.LogWarning("File attachments could not be processed because the Pandoc version check failed."); if (existingDocuments.Any(document => FileTypes.RequiresPandoc(document.FilePath)))
else
{ {
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 // The file is usable, but we lost parts of it. The user has to know which
// files when at least one of them could actually be read. Announcing files we // parts are missing, because the answer will be based on the rest.
// then hand over as empty blocks makes the AI answer about an empty document.
// //
var documentBlocks = new StringBuilder(); if (extraction.Outcome is FileExtractionOutcome.PARTIAL)
foreach(var document in existingDocuments)
{ {
if (document.IsForbidden) 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)));
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("````");
} }
if (documentBlocks.Length > 0) documentBlocks.AppendLine();
{ documentBlocks.AppendLine("---------------------------------------");
sb.AppendLine(); documentBlocks.AppendLine($"File path: {document.FilePath}");
sb.AppendLine("The following files are attached to this message:"); documentBlocks.AppendLine("File content:");
sb.Append(documentBlocks); documentBlocks.AppendLine("````");
} documentBlocks.AppendLine(extraction.Content);
documentBlocks.AppendLine("````");
}
var numImages = normalizedAttachments.Count(x => x is { IsImage: true, Exists: true }); if (documentBlocks.Length > 0)
if (numImages > 0) {
{ sb.AppendLine();
sb.AppendLine(); sb.AppendLine("The following files are attached to this message:");
sb.AppendLine($"Additionally, there are {numImages} image file(s) attached to this message. "); sb.Append(documentBlocks);
sb.AppendLine("Please consider them as part of the message content and use them to answer accordingly."); }
}
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.");
} }
} }
} }

View File

@ -443,19 +443,27 @@ public partial class AttachDocuments : MSGComponentBase
var mediaPaths = existingPaths.Where(IsTranscribableMedia).ToList(); var mediaPaths = existingPaths.Where(IsTranscribableMedia).ToList();
var regularPaths = existingPaths.Except(mediaPaths).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( var pandocState = await this.PandocAvailabilityService.EnsureAvailabilityAsync(
showSuccessMessage: false, showSuccessMessage: false,
showDialog: true); showDialog: true);
canAddRegularFiles = pandocState.IsAvailable; canAddPandocFiles = pandocState.IsAvailable;
} }
foreach (var path in regularPaths) foreach (var path in regularPaths)
{ {
if (!canAddRegularFiles) if (!canAddPandocFiles && FileTypes.RequiresPandoc(path))
break; {
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)) if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider))
continue; continue;

View File

@ -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 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"); public static readonly FileTypeFilter PLUGIN_ARCHIVE = FileTypeFilter.Leaf(TB("Plugin archive"), PluginArchive.PLUGIN_FILE_EXTENSION.TrimStart('.'), "zip");
/// <summary>
/// The file types AI Studio converts using Pandoc.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
private static readonly FileTypeFilter PANDOC_CONVERTED = FileTypeFilter.Leaf("Pandoc conversion", "docx", "odt", "html", "htm");
/// <summary>
/// Determines whether reading the given file needs Pandoc.
/// </summary>
/// <param name="filePath">The path of the file to check.</param>
/// <returns>True, when reading the file needs Pandoc.</returns>
public static bool RequiresPandoc(string filePath) => IsAllowedPath(filePath, PANDOC_CONVERTED);
public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types) public static FileTypeFilter? AsOneFileType(params FileTypeFilter[]? types)
{ {
if (types == null || types.Length == 0) if (types == null || types.Length == 0)