mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 18:32:12 +00:00
Added structured extraction errors to the content stream handling
This commit is contained in:
parent
47059352f1
commit
4790439f9e
38
app/MindWork AI Studio/Tools/ContentStreamErrorDetails.cs
Normal file
38
app/MindWork AI Studio/Tools/ContentStreamErrorDetails.cs
Normal file
@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The page the failure belongs to, when the failure affects a single page only.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("page_number")]
|
||||||
|
public int? PageNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parsed error code.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Codes this version does not know map to <see cref="FileExtractionErrorCode.UNKNOWN"/>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
|
public FileExtractionErrorCode ParsedCode => Enum.TryParse<FileExtractionErrorCode>(this.Code, ignoreCase: true, out var parsedCode) ? parsedCode : FileExtractionErrorCode.UNKNOWN;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this failure affects one part of the file only, while the
|
||||||
|
/// remaining content is still usable.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsPartialFailure => this.ParsedCode is FileExtractionErrorCode.PAGE_EXTRACTION_FAILED;
|
||||||
|
}
|
||||||
11
app/MindWork AI Studio/Tools/ContentStreamErrorMetadata.cs
Normal file
11
app/MindWork AI Studio/Tools/ContentStreamErrorMetadata.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ public sealed class ContentStreamMetadataJsonConverter : JsonConverter<ContentSt
|
|||||||
"Presentation" => JsonSerializer.Deserialize<ContentStreamPresentationMetadata?>(rawText, options),
|
"Presentation" => JsonSerializer.Deserialize<ContentStreamPresentationMetadata?>(rawText, options),
|
||||||
"Image" => JsonSerializer.Deserialize<ContentStreamImageMetadata?>(rawText, options),
|
"Image" => JsonSerializer.Deserialize<ContentStreamImageMetadata?>(rawText, options),
|
||||||
"Document" => JsonSerializer.Deserialize<ContentStreamDocumentMetadata?>(rawText, options),
|
"Document" => JsonSerializer.Deserialize<ContentStreamDocumentMetadata?>(rawText, options),
|
||||||
|
"Error" => JsonSerializer.Deserialize<ContentStreamErrorMetadata?>(rawText, options),
|
||||||
|
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|||||||
23
app/MindWork AI Studio/Tools/ContentStreamProcessedEvent.cs
Normal file
23
app/MindWork AI Studio/Tools/ContentStreamProcessedEvent.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The outcome of processing one content stream event: either content to append, or a reported
|
||||||
|
/// failure.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="Content">The content to append, or null when this event carries none.</param>
|
||||||
|
/// <param name="Error">The reported failure, or null when the event was processed successfully.</param>
|
||||||
|
public readonly record struct ContentStreamProcessedEvent(string? Content, ContentStreamErrorDetails? Error)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An event which neither produced content nor reported a failure.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ public static class ContentStreamSseHandler
|
|||||||
private static readonly ConcurrentDictionary<string, List<ContentStreamPptxImageData>> CHUNKED_IMAGES = new();
|
private static readonly ConcurrentDictionary<string, List<ContentStreamPptxImageData>> CHUNKED_IMAGES = new();
|
||||||
private static readonly ConcurrentDictionary<string, SlideManager> SLIDE_MANAGERS = new();
|
private static readonly ConcurrentDictionary<string, SlideManager> SLIDE_MANAGERS = new();
|
||||||
|
|
||||||
public static string? ProcessEvent(ContentStreamSseEvent? sseEvent, bool extractImages = true)
|
public static ContentStreamProcessedEvent ProcessEvent(ContentStreamSseEvent? sseEvent, bool extractImages = true)
|
||||||
{
|
{
|
||||||
switch (sseEvent)
|
switch (sseEvent)
|
||||||
{
|
{
|
||||||
@ -16,15 +16,15 @@ public static class ContentStreamSseHandler
|
|||||||
switch (sseEvent.Metadata)
|
switch (sseEvent.Metadata)
|
||||||
{
|
{
|
||||||
case ContentStreamTextMetadata:
|
case ContentStreamTextMetadata:
|
||||||
return sseEvent.Content;
|
return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
|
||||||
|
|
||||||
case ContentStreamPdfMetadata pdfMetadata:
|
case ContentStreamPdfMetadata pdfMetadata:
|
||||||
var pageNumber = pdfMetadata.Pdf?.PageNumber ?? 0;
|
var pageNumber = pdfMetadata.Pdf?.PageNumber ?? 0;
|
||||||
return $"""
|
return ContentStreamProcessedEvent.FromContent($"""
|
||||||
# Page {pageNumber}
|
# Page {pageNumber}
|
||||||
{sseEvent.Content}
|
{sseEvent.Content}
|
||||||
|
|
||||||
""";
|
""");
|
||||||
|
|
||||||
case ContentStreamSpreadsheetMetadata spreadsheetMetadata:
|
case ContentStreamSpreadsheetMetadata spreadsheetMetadata:
|
||||||
var sheetName = spreadsheetMetadata.Spreadsheet?.SheetName;
|
var sheetName = spreadsheetMetadata.Spreadsheet?.SheetName;
|
||||||
@ -37,11 +37,11 @@ public static class ContentStreamSseHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
spreadSheetResult.Append(sseEvent.Content);
|
spreadSheetResult.Append(sseEvent.Content);
|
||||||
return spreadSheetResult.ToString();
|
return ContentStreamProcessedEvent.FromContent(spreadSheetResult.ToString());
|
||||||
|
|
||||||
case ContentStreamDocumentMetadata:
|
case ContentStreamDocumentMetadata:
|
||||||
case ContentStreamImageMetadata:
|
case ContentStreamImageMetadata:
|
||||||
return sseEvent.Content;
|
return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
|
||||||
|
|
||||||
case ContentStreamPresentationMetadata presentationMetadata:
|
case ContentStreamPresentationMetadata presentationMetadata:
|
||||||
var slideManager = SLIDE_MANAGERS.GetOrAdd(
|
var slideManager = SLIDE_MANAGERS.GetOrAdd(
|
||||||
@ -50,17 +50,25 @@ public static class ContentStreamSseHandler
|
|||||||
);
|
);
|
||||||
|
|
||||||
slideManager.AddSlide(presentationMetadata, sseEvent.Content, extractImages);
|
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:
|
default:
|
||||||
return sseEvent.Content;
|
return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
|
||||||
}
|
}
|
||||||
|
|
||||||
case { Content: not null, Metadata: null }:
|
case { Content: not null, Metadata: null }:
|
||||||
return sseEvent.Content;
|
return ContentStreamProcessedEvent.FromContent(sseEvent.Content);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return ContentStreamProcessedEvent.NOTHING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
Normal file
26
app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public enum FileExtractionErrorCode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A code this version does not know, e.g. from a newer runtime.
|
||||||
|
/// </summary>
|
||||||
|
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,
|
||||||
|
}
|
||||||
@ -48,9 +48,19 @@ public sealed partial class RustService
|
|||||||
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
|
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
|
||||||
if (sseEvent is not null)
|
if (sseEvent is not null)
|
||||||
{
|
{
|
||||||
var content = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
|
var processedEvent = ContentStreamSseHandler.ProcessEvent(sseEvent, extractImages);
|
||||||
if (content is not null)
|
if (processedEvent.Error is not null)
|
||||||
resultBuilder.AppendLine(content);
|
{
|
||||||
|
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++;
|
chunkCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user