mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 17:52:10 +00:00
Improved file loading so every caller reports failures instead of empty content
This commit is contained in:
parent
582346644d
commit
c7a88e81b9
@ -716,7 +716,21 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileContent = (await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue)).Content;
|
||||
var extraction = await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue);
|
||||
if (!extraction.HasUsableContent)
|
||||
{
|
||||
this.Logger.LogError("Reading the document '{FilePath}' failed and it will not be analyzed: code={ErrorCode}, message='{ErrorMessage}'.", document.FilePath, extraction.ErrorCode, extraction.ErrorMessage);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Description, extraction.ToUserMessage(document.FileName)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extraction.Outcome is FileExtractionOutcome.PARTIAL)
|
||||
{
|
||||
this.Logger.LogWarning("Parts of the document '{FilePath}' could not be read: pages={FailedPages}.", document.FilePath, string.Join(", ", extraction.FailedPages));
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(document.FileName)));
|
||||
}
|
||||
|
||||
var fileContent = extraction.Content;
|
||||
sb.AppendLine($"""
|
||||
|
||||
## DOCUMENT {numDocuments}:
|
||||
|
||||
@ -579,9 +579,10 @@ public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialog
|
||||
try
|
||||
{
|
||||
this.isLoadingCustomPromptGuide = true;
|
||||
this.customPromptingGuidelineContent = await UserFile.LoadFileData(fileAttachment.FilePath, this.RustService, this.DialogService);
|
||||
if (string.IsNullOrWhiteSpace(this.customPromptingGuidelineContent))
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Description, T("The custom prompt guide file is empty or could not be read.")));
|
||||
|
||||
// A failure was already reported by UserFile.LoadFileData, so we only keep the content:
|
||||
var extraction = await UserFile.LoadFileData(fileAttachment.FilePath, this.RustService, this.DialogService);
|
||||
this.customPromptingGuidelineContent = extraction.HasUsableContent ? extraction.Content : string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@ -382,7 +382,21 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileContent = (await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue)).Content;
|
||||
var extraction = await this.RustService.ReadArbitraryFileData(document.FilePath, int.MaxValue);
|
||||
if (!extraction.HasUsableContent)
|
||||
{
|
||||
this.Logger.LogError("Reading the document '{FilePath}' failed and it will not be used: code={ErrorCode}, message='{ErrorMessage}'.", document.FilePath, extraction.ErrorCode, extraction.ErrorMessage);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.Description, extraction.ToUserMessage(document.FileName)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extraction.Outcome is FileExtractionOutcome.PARTIAL)
|
||||
{
|
||||
this.Logger.LogWarning("Parts of the document '{FilePath}' could not be read: pages={FailedPages}.", document.FilePath, string.Join(", ", extraction.FailedPages));
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Description, extraction.ToPartialUserMessage(document.FileName)));
|
||||
}
|
||||
|
||||
var fileContent = extraction.Content;
|
||||
sb.AppendLine($"""
|
||||
|
||||
## DOCUMENT {numDocuments}:
|
||||
|
||||
@ -317,7 +317,7 @@ public sealed class ContentText : IContent
|
||||
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)));
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, FileExtractionErrorCode.PANDOC_UNAVAILABLE.ToUserMessage(document.FileName)));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -324,8 +324,13 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
|
||||
try
|
||||
{
|
||||
var fileContent = await UserFile.LoadFileData(filePath, this.RustService, this.DialogService);
|
||||
await this.ApplyFileContentAsync(fileContent, filePath);
|
||||
var extraction = await UserFile.LoadFileData(filePath, this.RustService, this.DialogService);
|
||||
|
||||
// The failure was already reported by UserFile.LoadFileData, so we only stop here:
|
||||
if (!extraction.HasUsableContent)
|
||||
return false;
|
||||
|
||||
await this.ApplyFileContentAsync(extraction.Content, filePath);
|
||||
this.Logger.LogInformation("Successfully loaded file content: {FilePath}", filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -33,6 +33,12 @@
|
||||
@T("The specified file could not be found. The file have been moved, deleted, renamed, or is otherwise inaccessible.")
|
||||
</MudAlert>
|
||||
}
|
||||
else if (this.loadFailureMessage is not null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled" Class="my-2">
|
||||
@this.loadFailureMessage
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" Outlined="true" PanelClass="pa-2" Class="mb-2">
|
||||
|
||||
@ -21,6 +21,11 @@ public partial class DocumentCheckDialog : MSGComponentBase
|
||||
[Parameter]
|
||||
public string FileContent { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Set when reading the file failed, so the dialog shows the reason instead of empty content.
|
||||
/// </summary>
|
||||
private string? loadFailureMessage;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
@ -38,14 +43,22 @@ public partial class DocumentCheckDialog : MSGComponentBase
|
||||
{
|
||||
if (!this.Document.IsImage)
|
||||
{
|
||||
var fileContent = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.DialogService);
|
||||
this.FileContent = fileContent;
|
||||
var extraction = await UserFile.LoadFileData(this.Document.FilePath, this.RustService, this.DialogService);
|
||||
this.FileContent = extraction.Content;
|
||||
|
||||
//
|
||||
// This dialog exists so the user can check what we hand to the AI. Showing an
|
||||
// empty document when reading the file failed would answer that question wrong.
|
||||
//
|
||||
if (!extraction.HasUsableContent)
|
||||
this.loadFailureMessage = extraction.ToUserMessage(this.Document.FileName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Logger.LogError(ex, "Failed to load file content from '{FilePath}'", this.Document);
|
||||
this.FileContent = string.Empty;
|
||||
this.loadFailureMessage = FileExtractionErrorCode.INTERNAL.ToUserMessage(this.Document.FileName);
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
|
||||
@ -37,6 +37,11 @@ public enum FileExtractionErrorCode
|
||||
// Codes reported by the app itself:
|
||||
//
|
||||
|
||||
/// <summary>
|
||||
/// Reading the file needs Pandoc, which is not available.
|
||||
/// </summary>
|
||||
PANDOC_UNAVAILABLE,
|
||||
|
||||
/// <summary>
|
||||
/// The runtime answered with an unsuccessful HTTP status.
|
||||
/// </summary>
|
||||
|
||||
@ -20,7 +20,19 @@ internal static class FileExtractionResultExtensions
|
||||
/// <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);
|
||||
internal static string ToUserMessage(this FileExtractionResult result, string fileName) => result.ErrorCode.ToUserMessage(fileName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message which explains why a file could not be read.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This overload exists for the places which know the reason before an extraction was even
|
||||
/// attempted, so both ways of skipping a file tell the user the same thing.
|
||||
/// </remarks>
|
||||
/// <param name="code">The stable failure code.</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 FileExtractionErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized message for a file which was read, but lost some of its pages.
|
||||
@ -45,6 +57,8 @@ internal static class FileExtractionResultExtensions
|
||||
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.PANDOC_UNAVAILABLE => TB("Reading the file '{0}' needs Pandoc, which is not available, so the file 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."),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
@ -14,18 +15,31 @@ public static class UserFile
|
||||
/// <summary>
|
||||
/// Attempts to load the content of a file at the specified path, ensuring Pandoc is installed and available before proceeding.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the one place which reports a failed load to the user, so callers neither have to
|
||||
/// repeat that nor may they treat a failure as an empty file.
|
||||
/// </remarks>
|
||||
/// <param name="filePath">The full path to the file to be read. Must not be null or empty.</param>
|
||||
/// <param name="rustService">Rust service used to read file content.</param>
|
||||
/// <param name="dialogService">Dialogservice used to display the Pandoc installation dialog if needed.</param>
|
||||
public static async Task<string> LoadFileData(string filePath, RustService rustService, IDialogService dialogService)
|
||||
/// <returns>The result of reading the file.</returns>
|
||||
public static async Task<FileExtractionResult> LoadFileData(string filePath, RustService rustService, IDialogService dialogService)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
LOGGER.LogError("Can't load from an empty or null file path.");
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("The file path is null or empty and the file therefore can not be loaded.")));
|
||||
return FileExtractionResult.Failed(FileExtractionErrorCode.INVALID_REQUEST, "The file path is null or empty.");
|
||||
}
|
||||
|
||||
// Ensure that Pandoc is installed and ready:
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
//
|
||||
// Ensure that Pandoc is installed and ready. This is only needed for the formats we
|
||||
// convert with it: PDFs and the other document types are read by the Rust runtime itself.
|
||||
//
|
||||
if (FileTypes.RequiresPandoc(filePath))
|
||||
{
|
||||
var pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: false);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
@ -40,12 +54,25 @@ public static class UserFile
|
||||
pandocState = await Pandoc.CheckAvailabilityAsync(rustService, showSuccessMessage: true);
|
||||
if (!pandocState.IsAvailable)
|
||||
{
|
||||
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.")));
|
||||
LOGGER.LogError("Pandoc is not available after installation attempt, so '{FilePath}' cannot be read.", filePath);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, FileExtractionErrorCode.PANDOC_UNAVAILABLE.ToUserMessage(fileName)));
|
||||
return FileExtractionResult.Failed(FileExtractionErrorCode.PANDOC_UNAVAILABLE, "Pandoc is required to read this file, but it is not available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileContent = await rustService.ReadArbitraryFileData(filePath, int.MaxValue);
|
||||
return fileContent.Content;
|
||||
var result = await rustService.ReadArbitraryFileData(filePath, int.MaxValue);
|
||||
if (!result.HasUsableContent)
|
||||
{
|
||||
LOGGER.LogError("Reading the file '{FilePath}' failed: code={ErrorCode}, message='{ErrorMessage}'.", filePath, result.ErrorCode, result.ErrorMessage);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Description, result.ToUserMessage(fileName)));
|
||||
}
|
||||
else if (result.Outcome is FileExtractionOutcome.PARTIAL)
|
||||
{
|
||||
LOGGER.LogWarning("Parts of the file '{FilePath}' could not be read: pages={FailedPages}.", filePath, string.Join(", ", result.FailedPages));
|
||||
await MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.Description, result.ToPartialUserMessage(fileName)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user