diff --git a/app/MindWork AI Studio/Tools/ContentStreamErrorDetails.cs b/app/MindWork AI Studio/Tools/ContentStreamErrorDetails.cs
new file mode 100644
index 00000000..fc3d11d7
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ContentStreamErrorDetails.cs
@@ -0,0 +1,38 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools;
+
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable ClassNeverInstantiated.Global
+public sealed class ContentStreamErrorDetails
+{
+ [JsonPropertyName("code")]
+ public string? Code { get; init; }
+
+ [JsonPropertyName("message")]
+ public string? Message { get; init; }
+
+ ///
+ /// The page the failure belongs to, when the failure affects a single page only.
+ ///
+ [JsonPropertyName("page_number")]
+ public int? PageNumber { get; init; }
+
+ ///
+ /// Gets the parsed error code.
+ ///
+ ///
+ /// Codes this version does not know map to
+ /// instead of failing the deserialization. A failed deserialization would turn the reported
+ /// error back into empty file content, which is exactly what we want to avoid here.
+ ///
+ [JsonIgnore]
+ public FileExtractionErrorCode ParsedCode => Enum.TryParse(this.Code, ignoreCase: true, out var parsedCode) ? parsedCode : FileExtractionErrorCode.UNKNOWN;
+
+ ///
+ /// Gets a value indicating whether this failure affects one part of the file only, while the
+ /// remaining content is still usable.
+ ///
+ [JsonIgnore]
+ public bool IsPartialFailure => this.ParsedCode is FileExtractionErrorCode.PAGE_EXTRACTION_FAILED;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ContentStreamErrorMetadata.cs b/app/MindWork AI Studio/Tools/ContentStreamErrorMetadata.cs
new file mode 100644
index 00000000..d32f24f9
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ContentStreamErrorMetadata.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools;
+
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable ClassNeverInstantiated.Global
+public sealed class ContentStreamErrorMetadata : ContentStreamSseMetadata
+{
+ [JsonPropertyName("Error")]
+ public ContentStreamErrorDetails? Error { get; init; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ContentStreamMetadataJsonConverter.cs b/app/MindWork AI Studio/Tools/ContentStreamMetadataJsonConverter.cs
index e3308c78..68dee19e 100644
--- a/app/MindWork AI Studio/Tools/ContentStreamMetadataJsonConverter.cs
+++ b/app/MindWork AI Studio/Tools/ContentStreamMetadataJsonConverter.cs
@@ -23,7 +23,8 @@ public sealed class ContentStreamMetadataJsonConverter : JsonConverter JsonSerializer.Deserialize(rawText, options),
"Image" => JsonSerializer.Deserialize(rawText, options),
"Document" => JsonSerializer.Deserialize(rawText, options),
-
+ "Error" => JsonSerializer.Deserialize(rawText, options),
+
_ => null
};
}
diff --git a/app/MindWork AI Studio/Tools/ContentStreamProcessedEvent.cs b/app/MindWork AI Studio/Tools/ContentStreamProcessedEvent.cs
new file mode 100644
index 00000000..726306b3
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/ContentStreamProcessedEvent.cs
@@ -0,0 +1,23 @@
+namespace AIStudio.Tools;
+
+///
+/// The outcome of processing one content stream event: either content to append, or a reported
+/// failure.
+///
+///
+/// Content and error are kept apart on purpose. A reported failure must never be appended as
+/// content, because that would hand the failure to the AI as if it were part of the document.
+///
+/// The content to append, or null when this event carries none.
+/// The reported failure, or null when the event was processed successfully.
+public readonly record struct ContentStreamProcessedEvent(string? Content, ContentStreamErrorDetails? Error)
+{
+ ///
+ /// An event which neither produced content nor reported a failure.
+ ///
+ public static readonly ContentStreamProcessedEvent NOTHING = new(null, null);
+
+ public static ContentStreamProcessedEvent FromContent(string? content) => new(content, null);
+
+ public static ContentStreamProcessedEvent FromError(ContentStreamErrorDetails? error) => new(null, error);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs b/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs
index 247d3ebf..37d354af 100644
--- a/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs
+++ b/app/MindWork AI Studio/Tools/ContentStreamSseHandler.cs
@@ -8,7 +8,7 @@ public static class ContentStreamSseHandler
private static readonly ConcurrentDictionary> CHUNKED_IMAGES = new();
private static readonly ConcurrentDictionary SLIDE_MANAGERS = new();
- public static string? ProcessEvent(ContentStreamSseEvent? sseEvent, bool extractImages = true)
+ public static ContentStreamProcessedEvent ProcessEvent(ContentStreamSseEvent? sseEvent, bool extractImages = true)
{
switch (sseEvent)
{
@@ -16,16 +16,16 @@ public static class ContentStreamSseHandler
switch (sseEvent.Metadata)
{
case ContentStreamTextMetadata:
- return sseEvent.Content;
-
+ return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
+
case ContentStreamPdfMetadata pdfMetadata:
var pageNumber = pdfMetadata.Pdf?.PageNumber ?? 0;
- return $"""
+ return ContentStreamProcessedEvent.FromContent($"""
# Page {pageNumber}
{sseEvent.Content}
-
- """;
-
+
+ """);
+
case ContentStreamSpreadsheetMetadata spreadsheetMetadata:
var sheetName = spreadsheetMetadata.Spreadsheet?.SheetName;
var rowNumber = spreadsheetMetadata.Spreadsheet?.RowNumber;
@@ -37,30 +37,38 @@ public static class ContentStreamSseHandler
}
spreadSheetResult.Append(sseEvent.Content);
- return spreadSheetResult.ToString();
-
+ return ContentStreamProcessedEvent.FromContent(spreadSheetResult.ToString());
+
case ContentStreamDocumentMetadata:
case ContentStreamImageMetadata:
- return sseEvent.Content;
+ return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
case ContentStreamPresentationMetadata presentationMetadata:
var slideManager = SLIDE_MANAGERS.GetOrAdd(
sseEvent.StreamId!,
_ => new()
);
-
+
slideManager.AddSlide(presentationMetadata, sseEvent.Content, extractImages);
- return null;
-
+ return ContentStreamProcessedEvent.NOTHING;
+
+ //
+ // The runtime reported a failure. It must not contribute any content: an empty
+ // or partial document would otherwise be handed to the AI as if it were the
+ // real file content.
+ //
+ case ContentStreamErrorMetadata errorMetadata:
+ return ContentStreamProcessedEvent.FromError(errorMetadata.Error);
+
default:
- return sseEvent.Content;
+ return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
}
-
+
case { Content: not null, Metadata: null }:
- return sseEvent.Content;
-
+ return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
+
default:
- return null;
+ return ContentStreamProcessedEvent.NOTHING;
}
}
diff --git a/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs b/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
new file mode 100644
index 00000000..80888cec
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
@@ -0,0 +1,26 @@
+namespace AIStudio.Tools;
+
+///
+/// Why reading a file failed. The Rust runtime reports these codes as part of the content
+/// stream, so the app can tell the user what happened instead of showing an empty document.
+///
+public enum FileExtractionErrorCode
+{
+ ///
+ /// A code this version does not know, e.g. from a newer runtime.
+ ///
+ UNKNOWN,
+
+ INVALID_REQUEST,
+ FILE_NOT_FOUND,
+ FILE_NOT_READABLE,
+ FORMAT_DETECTION_FAILED,
+ NOT_A_VALID_PDF,
+ NOT_A_VALID_SPREADSHEET,
+ PDFIUM_UNAVAILABLE,
+ PDF_ENCRYPTED,
+ PAGE_EXTRACTION_FAILED,
+ NO_TEXT_EXTRACTED,
+ UNSUPPORTED,
+ INTERNAL,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs
index 4a3f59d5..e503b793 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs
@@ -48,9 +48,19 @@ public sealed partial class RustService
var sseEvent = JsonSerializer.Deserialize(jsonContent);
if (sseEvent is not null)
{
- var content = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
- if (content is not null)
- resultBuilder.AppendLine(content);
+ var processedEvent = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
+ if (processedEvent.Error is not null)
+ {
+ this.logger?.LogError(
+ "The runtime reported a failure while reading '{Path}': code={ErrorCode}, page={PageNumber}, partial={IsPartialFailure}, message='{Message}'",
+ path,
+ processedEvent.Error.ParsedCode,
+ processedEvent.Error.PageNumber,
+ processedEvent.Error.IsPartialFailure,
+ processedEvent.Error.Message);
+ }
+ else if (processedEvent.Content is not null)
+ resultBuilder.AppendLine(processedEvent.Content);
chunkCount++;
}