using AIStudio.Tools.Security;
namespace AIStudio.Tools;
///
/// The result of reading a file through the Rust runtime.
///
///
/// Content and failure travel together on purpose. When reading a file returns a bare string, a
/// failed extraction is indistinguishable from an empty document, and the empty document reaches
/// the AI as if that were the content of the user's file.
///
/// How the extraction ended.
/// The extracted content. Empty when the extraction failed.
/// Why the extraction failed or lost parts of the file.
/// The technical failure description, meant for logs and diagnostics.
/// The pages which could not be read, when known.
/// The format the runtime identified by looking at the content, when it is worth naming.
public readonly record struct FileExtractionResult(FileExtractionOutcome Outcome, string Content, FileExtractionErrorCode ErrorCode, string? ErrorMessage, IReadOnlyList FailedPages, string? DetectedFormat)
{
private static readonly int[] NO_FAILED_PAGES = [];
private static readonly PromptInjectionFinding[] NO_FINDINGS = [];
private readonly IReadOnlyList? promptInjectionFindings;
///
/// The prompt-injection attempts the runtime filtered out of the content, if any.
///
///
/// This is a notice, not a failure: the passages were removed and the content around them
/// is intact, which is why it does not affect the outcome. The findings exist so the app
/// can tell the user what was removed from their document.
///
public IReadOnlyList PromptInjectionFindings
{
get => this.promptInjectionFindings ?? NO_FINDINGS;
init => this.promptInjectionFindings = value;
}
///
/// How many passages were filtered out. May exceed the number of findings, because the
/// runtime caps how many it reports in detail while it filters every single one.
///
public int PromptInjectionRedactedCount { get; init; }
///
/// Gets a value indicating whether prompt injections were filtered out of the content.
///
public bool HasFilteredPromptInjections => this.PromptInjectionRedactedCount > 0;
public static FileExtractionResult Success(string content, string? detectedFormat = null) => new(FileExtractionOutcome.SUCCESS, content, FileExtractionErrorCode.NONE, null, NO_FAILED_PAGES, detectedFormat);
public static FileExtractionResult Partial(string content, IReadOnlyList failedPages, string? detectedFormat = null) => new(FileExtractionOutcome.PARTIAL, content, FileExtractionErrorCode.PAGE_EXTRACTION_FAILED, null, failedPages, detectedFormat);
public static FileExtractionResult Failed(FileExtractionErrorCode errorCode, string? errorMessage, string? detectedFormat = null) => new(FileExtractionOutcome.FAILED, string.Empty, errorCode, errorMessage, NO_FAILED_PAGES, detectedFormat);
///
/// Gets a value indicating whether the whole file was read.
///
public bool IsSuccess => this.Outcome is FileExtractionOutcome.SUCCESS;
///
/// Gets a value indicating whether the content may be handed to the AI, i.e. the extraction
/// either succeeded or lost only parts of the file.
///
public bool HasUsableContent => this.Outcome is FileExtractionOutcome.SUCCESS or FileExtractionOutcome.PARTIAL;
///
/// Gets a value indicating whether the file was read, but its content did not match its file
/// extension.
///
///
/// On a readable file, only the mismatch notice names a detected format, which is why no
/// separate flag is needed here.
///
public bool HasExtensionMismatch => this.HasUsableContent && this.DetectedFormat is not null;
}