mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 16:32:10 +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,7 +23,8 @@ public sealed class ContentStreamMetadataJsonConverter : JsonConverter<ContentSt
|
||||
"Presentation" => JsonSerializer.Deserialize<ContentStreamPresentationMetadata?>(rawText, options),
|
||||
"Image" => JsonSerializer.Deserialize<ContentStreamImageMetadata?>(rawText, options),
|
||||
"Document" => JsonSerializer.Deserialize<ContentStreamDocumentMetadata?>(rawText, options),
|
||||
|
||||
"Error" => JsonSerializer.Deserialize<ContentStreamErrorMetadata?>(rawText, options),
|
||||
|
||||
_ => 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, SlideManager> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
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++;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user