mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 16:32:10 +00:00
Fixed empty file content being sent to the AI when reading a file fails
This commit is contained in:
parent
8d91b1cda8
commit
cf4661d1ad
@ -14,6 +14,7 @@ namespace AIStudio.Chat;
|
||||
public sealed class ContentText : IContent
|
||||
{
|
||||
private static readonly ILogger<ContentText> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ContentText>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ContentText).Namespace, nameof(ContentText));
|
||||
|
||||
/// <summary>
|
||||
@ -266,12 +267,18 @@ public sealed class ContentText : IContent
|
||||
// Get the list of existing documents:
|
||||
var existingDocuments = normalizedAttachments.Where(x => x.Type is FileAttachmentType.DOCUMENT && x.Exists).ToList();
|
||||
|
||||
// Log warning for missing files:
|
||||
//
|
||||
// Report missing files. We tell the user about them instead of only logging: on a
|
||||
// network drive, a file which is temporarily unreachable looks exactly like a deleted
|
||||
// one, and silently dropping it would let the AI answer without that document.
|
||||
//
|
||||
var missingDocuments = normalizedAttachments.Except(existingDocuments).Where(x => x.Type is FileAttachmentType.DOCUMENT).ToList();
|
||||
if (missingDocuments.Count > 0)
|
||||
foreach (var missingDocument in missingDocuments)
|
||||
LOGGER.LogWarning("File attachment no longer exists and will be skipped: '{MissingDocument}'.", missingDocument.FilePath);
|
||||
|
||||
foreach (var missingDocument in missingDocuments)
|
||||
{
|
||||
LOGGER.LogWarning("File attachment no longer exists and will be skipped: '{MissingDocument}'.", missingDocument.FilePath);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.FindInPage, string.Format(TB("The file '{0}' is currently not available and was not sent."), missingDocument.FileName)));
|
||||
}
|
||||
|
||||
// Only proceed if there are existing, allowed documents:
|
||||
if (existingDocuments.Count > 0)
|
||||
{
|
||||
@ -284,8 +291,12 @@ public sealed class ContentText : IContent
|
||||
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:");
|
||||
//
|
||||
// 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)
|
||||
@ -293,16 +304,41 @@ public sealed class ContentText : IContent
|
||||
LOGGER.LogWarning("File attachment '{FilePath}' has a forbidden file type and will be skipped.", document.FilePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("---------------------------------------");
|
||||
sb.AppendLine($"File path: {document.FilePath}");
|
||||
sb.AppendLine("File content:");
|
||||
sb.AppendLine("````");
|
||||
sb.AppendLine((await Program.RUST_SERVICE.ReadArbitraryFileData(document.FilePath, int.MaxValue)).Content);
|
||||
sb.AppendLine("````");
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -321,4 +357,4 @@ public sealed class ContentText : IContent
|
||||
/// The text content.
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Translates the stable failure codes of a file extraction into user-facing text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The message which travels with a result is technical: it comes from the runtime, names the
|
||||
/// library which failed, and belongs into the log. The texts here are the counterpart for the
|
||||
/// user, and they name what the user can act on, such as an unavailable network drive.
|
||||
/// </remarks>
|
||||
internal static class FileExtractionResultExtensions
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionResultExtensions).Namespace, nameof(FileExtractionResultExtensions));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why a file could not be read.
|
||||
/// </summary>
|
||||
/// <param name="result">The extraction result.</param>
|
||||
/// <param name="fileName">The name of the file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
internal static string ToUserMessage(this FileExtractionResult result, string fileName) => string.Format(ToUserMessageFormat(result.ErrorCode), fileName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message for a file which was read, but lost some of its pages.
|
||||
/// </summary>
|
||||
/// <param name="result">The extraction result.</param>
|
||||
/// <param name="fileName">The name of the file, as shown to the user.</param>
|
||||
/// <returns>The localized message.</returns>
|
||||
internal static string ToPartialUserMessage(this FileExtractionResult result, string fileName)
|
||||
{
|
||||
if (result.FailedPages.Count == 0)
|
||||
return string.Format(TB("Parts of the file '{0}' could not be read. The remaining content was sent."), fileName);
|
||||
|
||||
return string.Format(TB("The pages {1} of the file '{0}' could not be read. The remaining content was sent."), fileName, string.Join(", ", result.FailedPages));
|
||||
}
|
||||
|
||||
private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch
|
||||
{
|
||||
FileExtractionErrorCode.FILE_NOT_FOUND => TB("The file '{0}' does not exist anymore and was not sent."),
|
||||
FileExtractionErrorCode.FILE_NOT_READABLE => TB("The file '{0}' could not be read and was not sent. When the file is stored on a network drive, the drive might be unavailable, or another program might be blocking the file."),
|
||||
FileExtractionErrorCode.TIMEOUT => TB("Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted."),
|
||||
FileExtractionErrorCode.NOT_A_VALID_PDF => TB("The file '{0}' is not a readable PDF and was not sent. It might be damaged or transferred incompletely."),
|
||||
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely."),
|
||||
FileExtractionErrorCode.PDF_ENCRYPTED => TB("The file '{0}' is protected and could not be opened, so it was not sent."),
|
||||
FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("AI Studio was not able to start its PDF engine, so the file '{0}' was not sent."),
|
||||
FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No text could be read from the file '{0}', so it was not sent. The file might consist of scanned images without a text layer."),
|
||||
FileExtractionErrorCode.NO_CONTENT => TB("The file '{0}' did not provide any content and was not sent."),
|
||||
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("The file type of '{0}' could not be determined, so the file was not sent."),
|
||||
FileExtractionErrorCode.UNSUPPORTED => TB("The file type of '{0}' is not supported, so the file was not sent."),
|
||||
|
||||
_ => TB("The file '{0}' could not be read and was not sent."),
|
||||
};
|
||||
}
|
||||
@ -117,6 +117,7 @@ public sealed partial class RustService : BackgroundService
|
||||
public override void Dispose()
|
||||
{
|
||||
this.http.Dispose();
|
||||
this.extractionHttp.Dispose();
|
||||
this.userLanguageLock.Dispose();
|
||||
this.userNameLock.Dispose();
|
||||
base.Dispose();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user