From 7b96a57b5c14bb2986e81f83cfd8736c556e30d3 Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Mon, 27 Apr 2026 20:12:25 -0400 Subject: [PATCH 1/7] feat: Add CommunityToolkit.DocumentProcessing packages (PdfPig ONNX + DataIngestion) Port intelligent PDF ingestion packages from luisquintanilla/PdfPig (feature/intelligent-pdf-ingestion branch, PRs #1 and #2) into the AI Community Toolkit as a new MEDI top-level feature area. New packages: - CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis ONNX-based layout detection (RT-DETR, configurable models, image preprocessing, NMS) implementing PdfPig IPageSegmenter - CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion MEDI bridge with vision LLM enrichers (OCR, table extraction, contextual chunking) for PDF document ingestion Infrastructure: - MEDI/ directory structure following MEVD pattern - Directory.Build.props for src and test - Central package management updates - MEDI.slnf solution filter - medi.yml CI workflow - nuget.config with source mapping for dotnet-public feed Test results: 216 total (213 passed, 3 skipped for missing ONNX model) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/medi.yml | 64 +++ CommunityToolkit.AI.slnx | 9 + Directory.Packages.props | 13 + MEDI/MEDI.slnf | 13 + MEDI/src/Directory.Build.props | 13 + .../MetadataAwareSectionChunker.cs | 120 +++++ .../PdfPig.DataIngestion/PageImageRenderer.cs | 144 +++++ .../PdfPig.DataIngestion.csproj | 29 + ...ataIngestionServiceCollectionExtensions.cs | 40 ++ MEDI/src/PdfPig.DataIngestion/PdfPigReader.cs | 159 ++++++ .../PdfPigReaderOptions.cs | 18 + .../PdfPigReaderOptionsValidator.cs | 20 + .../PdfPig.DataIngestion/PdfReadingMode.cs | 28 + .../Processors/ContextualChunkEnricher.cs | 73 +++ .../Processors/VisionOcrEnricher.cs | 123 +++++ .../Processors/VisionTableEnricher.cs | 95 ++++ .../AnnotatedTextBlock.cs | 43 ++ .../DetectionPostprocessing.cs | 213 ++++++++ .../ILayoutDetectionModel.cs | 41 ++ .../ImagePreprocessing.cs | 174 ++++++ .../LayoutDetection.cs | 12 + .../Models/ConfigurableLayoutModel.cs | 468 ++++++++++++++++ .../Models/LayoutModelOptions.cs | 125 +++++ .../Models/RtDetrLayoutModel.cs | 265 ++++++++++ .../OnnxPageSegmenter.cs | 293 ++++++++++ ...ageSegmenterServiceCollectionExtensions.cs | 50 ++ .../OnnxSegmenterOptions.cs | 24 + .../OnnxSegmenterOptionsValidator.cs | 25 + .../PageImageRenderer.cs | 77 +++ .../PdfPig.OnnxLayoutAnalysis.csproj | 27 + MEDI/test/Directory.Build.props | 16 + .../ContextualChunkEnricherTests.cs | 197 +++++++ .../Documents/cat-genetics.pdf | Bin 0 -> 516459 bytes .../Documents/data.pdf | Bin 0 -> 28330 bytes .../Documents/inherited_mediabox.pdf | Bin 0 -> 1462 bytes .../IntegrationHelpers.cs | 33 ++ .../MetadataAwareSectionChunkerTests.cs | 179 +++++++ .../PageImageRendererTests.cs | 92 ++++ .../PdfPig.DataIngestion.UnitTests.csproj | 41 ++ .../PdfPigReaderDiTests.cs | 118 +++++ .../PdfPigReaderOptionsValidatorTests.cs | 82 +++ .../PdfPigReaderTests.cs | 499 ++++++++++++++++++ .../VisionOcrEnricherTests.cs | 292 ++++++++++ .../VisionTableEnricherTests.cs | 242 +++++++++ .../AnnotatedTextBlockTests.cs | 61 +++ .../ConfigurableLayoutModelTests.cs | 82 +++ .../DetectionPostprocessingTests.cs | 314 +++++++++++ .../ImagePreprocessingTests.cs | 391 ++++++++++++++ .../LayoutModelOptionsTests.cs | 241 +++++++++ .../OnnxPageSegmenterDiTests.cs | 141 +++++ .../OnnxPageSegmenterTests.cs | 311 +++++++++++ .../OnnxSegmenterOptionsValidatorTests.cs | 129 +++++ .../PageImageRendererTests.cs | 94 ++++ ...PdfPig.OnnxLayoutAnalysis.UnitTests.csproj | 35 ++ .../RtDetrLayoutModelTests.cs | 154 ++++++ nuget.config | 16 + 56 files changed, 6558 insertions(+) create mode 100644 .github/workflows/medi.yml create mode 100644 MEDI/MEDI.slnf create mode 100644 MEDI/src/Directory.Build.props create mode 100644 MEDI/src/PdfPig.DataIngestion/MetadataAwareSectionChunker.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/PageImageRenderer.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/PdfPig.DataIngestion.csproj create mode 100644 MEDI/src/PdfPig.DataIngestion/PdfPigDataIngestionServiceCollectionExtensions.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/PdfPigReader.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptions.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptionsValidator.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/PdfReadingMode.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/Processors/ContextualChunkEnricher.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/Processors/VisionOcrEnricher.cs create mode 100644 MEDI/src/PdfPig.DataIngestion/Processors/VisionTableEnricher.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/AnnotatedTextBlock.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/DetectionPostprocessing.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/ILayoutDetectionModel.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/ImagePreprocessing.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/LayoutDetection.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/ConfigurableLayoutModel.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/LayoutModelOptions.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/RtDetrLayoutModel.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenter.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenterServiceCollectionExtensions.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptions.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptionsValidator.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/PageImageRenderer.cs create mode 100644 MEDI/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj create mode 100644 MEDI/test/Directory.Build.props create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/ContextualChunkEnricherTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/cat-genetics.pdf create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/data.pdf create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/inherited_mediabox.pdf create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/IntegrationHelpers.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/MetadataAwareSectionChunkerTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/PageImageRendererTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPig.DataIngestion.UnitTests.csproj create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderDiTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderOptionsValidatorTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/VisionOcrEnricherTests.cs create mode 100644 MEDI/test/PdfPig.DataIngestion.UnitTests/VisionTableEnricherTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/AnnotatedTextBlockTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ConfigurableLayoutModelTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/DetectionPostprocessingTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ImagePreprocessingTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/LayoutModelOptionsTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterDiTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxSegmenterOptionsValidatorTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PageImageRendererTests.cs create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj create mode 100644 MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/RtDetrLayoutModelTests.cs create mode 100644 nuget.config diff --git a/.github/workflows/medi.yml b/.github/workflows/medi.yml new file mode 100644 index 0000000..b8e774d --- /dev/null +++ b/.github/workflows/medi.yml @@ -0,0 +1,64 @@ +name: MEDI + +on: + push: + branches: [main] + paths: + - 'MEDI/**' + - 'Shared/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'global.json' + - '.github/workflows/medi.yml' + pull_request: + branches: [main] + paths: + - 'MEDI/**' + - 'Shared/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'global.json' + - '.github/workflows/medi.yml' + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Build + run: dotnet build MEDI/MEDI.slnf + + unit-tests: + name: 'Unit: ${{ matrix.package }} (${{ matrix.os }})' + needs: build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + package: + - PdfPig.OnnxLayoutAnalysis + - PdfPig.DataIngestion + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Test + run: dotnet test MEDI/test/${{ matrix.package }}.UnitTests diff --git a/CommunityToolkit.AI.slnx b/CommunityToolkit.AI.slnx index 79732c7..8948b10 100644 --- a/CommunityToolkit.AI.slnx +++ b/CommunityToolkit.AI.slnx @@ -6,6 +6,15 @@ + + + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 7199d1e..7968ba1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,6 +22,17 @@ + + + + + + + + + + + @@ -36,6 +47,8 @@ + + diff --git a/MEDI/MEDI.slnf b/MEDI/MEDI.slnf new file mode 100644 index 0000000..3330c32 --- /dev/null +++ b/MEDI/MEDI.slnf @@ -0,0 +1,13 @@ +{ + "solution": { + "path": "../CommunityToolkit.AI.slnx", + "projects": + [ + "MEDI/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj", + "MEDI/src/PdfPig.DataIngestion/PdfPig.DataIngestion.csproj", + + "MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj", + "MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPig.DataIngestion.UnitTests.csproj" + ] + } +} diff --git a/MEDI/src/Directory.Build.props b/MEDI/src/Directory.Build.props new file mode 100644 index 0000000..a2ca3cb --- /dev/null +++ b/MEDI/src/Directory.Build.props @@ -0,0 +1,13 @@ + + + + + + true + + + + + + + diff --git a/MEDI/src/PdfPig.DataIngestion/MetadataAwareSectionChunker.cs b/MEDI/src/PdfPig.DataIngestion/MetadataAwareSectionChunker.cs new file mode 100644 index 0000000..8f3647d --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/MetadataAwareSectionChunker.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.DataIngestion.Chunkers; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; + +/// +/// A chunker that wraps and propagates element metadata +/// (such as "element_type") to the resulting chunks. This works around the fact that +/// the built-in MEDI chunkers do not copy element metadata to chunks. +/// +/// +/// After the inner chunker produces chunks, this class matches each chunk's content +/// back to its source elements via substring containment and copies metadata from +/// the matching elements to the chunk. For keys with conflicting values across +/// multiple matching elements, the value from the first match wins. +/// +public sealed class MetadataAwareSectionChunker : IngestionChunker +{ + private readonly SectionChunker _inner; + + /// + /// Creates a new . + /// + /// The chunker options (tokenizer, max tokens, overlap). + public MetadataAwareSectionChunker(IngestionChunkerOptions options) + { + _inner = new SectionChunker(options); + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IngestionDocument document, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var elementIndex = BuildElementIndex(document); + + await foreach (var chunk in _inner.ProcessAsync(document, cancellationToken).ConfigureAwait(false)) + { + ResolveMetadata(chunk, elementIndex); + yield return chunk; + } + } + + /// + /// Builds an index of element text snippets to their metadata. + /// Only includes elements that actually have metadata set. + /// + private static List<(string Text, IDictionary Metadata)> BuildElementIndex( + IngestionDocument document) + { + var index = new List<(string Text, IDictionary Metadata)>(); + + foreach (var section in document.Sections) + { + foreach (var element in section.Elements) + { + if (!element.HasMetadata) + { + continue; + } + + // Use the element's text content for matching. + // SectionChunker concatenates element GetMarkdown() values with newlines, + // so we match against GetMarkdown() for reliable substring containment. + var text = element.GetMarkdown(); + if (string.IsNullOrWhiteSpace(text)) + { + continue; + } + + index.Add((text, element.Metadata)); + } + } + + return index; + } + + /// + /// Matches a chunk's content back to source elements and copies their metadata. + /// For keys that appear in multiple matching elements, the first match's value wins. + /// + private static void ResolveMetadata( + IngestionChunk chunk, + List<(string Text, IDictionary Metadata)> elementIndex) + { + var content = chunk.Content; + if (string.IsNullOrEmpty(content)) + { + return; + } + + foreach (var (text, metadata) in elementIndex) + { + if (!content.Contains(text, StringComparison.Ordinal)) + { + continue; + } + + foreach (var kvp in metadata) + { + // Skip bounding box metadata — it's per-element, not meaningful at chunk level + if (kvp.Key.StartsWith("BoundingBox.", StringComparison.Ordinal)) + { + continue; + } + + // First match wins for each key + if (kvp.Value is not null && !chunk.Metadata.ContainsKey(kvp.Key)) + { + chunk.Metadata[kvp.Key] = kvp.Value; + } + } + } + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/PageImageRenderer.cs b/MEDI/src/PdfPig.DataIngestion/PageImageRenderer.cs new file mode 100644 index 0000000..830fa91 --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PageImageRenderer.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using SkiaSharp; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.Core; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; + +/// +/// Renders PDF pages to PNG images using SkiaSharp for use with vision LLMs. +/// Unlike DocumentLayoutAnalysis.Onnx.PageImageRenderer which renders word bounding +/// boxes as black rectangles for ML model input, this renderer draws actual text characters +/// to produce human/LLM-readable page images. +/// +public static class PageImageRenderer +{ + private const float DefaultFontScale = 12f; + private const float MinFontSize = 2f; + private const float PdfDpi = 72f; + private const int PngQuality = 90; + + /// + /// Renders an entire PDF page to a PNG byte array. + /// + /// The PDF page to render. + /// Target resolution in dots per inch. Defaults to 150. + /// A PNG-encoded byte array of the rendered page. + /// is . + public static byte[] RenderPage(Page page, int dpi = 150) + { + ArgumentNullException.ThrowIfNull(page); + + float scale = dpi / PdfDpi; + int pixelWidth = Math.Max(1, (int)(page.Width * scale)); + int pixelHeight = Math.Max(1, (int)(page.Height * scale)); + + using var bitmap = new SKBitmap(pixelWidth, pixelHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.White); + + using var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true }; + + var fontCache = new Dictionary(); + try + { + foreach (var letter in page.Letters) + { + float x = (float)letter.Location.X * scale; + float y = (float)(page.Height - letter.Location.Y) * scale; + float fontSize = (float)letter.PointSize * scale; + if (fontSize < MinFontSize) + { + fontSize = DefaultFontScale * scale; + } + + if (!fontCache.TryGetValue(fontSize, out var font)) + { + font = new SKFont { Size = fontSize }; + fontCache[fontSize] = font; + } + + canvas.DrawText(letter.Value, x, y, SKTextAlign.Left, font, paint); + } + } + finally + { + foreach (var font in fontCache.Values) + { + font.Dispose(); + } + } + + using var image = SKImage.FromBitmap(bitmap); + using var data = image.Encode(SKEncodedImageFormat.Png, PngQuality); + return data.ToArray(); + } + + /// + /// Renders a specific region of a PDF page to a PNG byte array. + /// Useful for sending only a table or element region to a vision LLM. + /// + /// The PDF page containing the region. + /// The bounding box of the region to render, in PDF coordinates. + /// Target resolution in dots per inch. Defaults to 150. + /// A PNG-encoded byte array of the rendered region. + /// is . + public static byte[] RenderRegion(Page page, PdfRectangle region, int dpi = 150) + { + ArgumentNullException.ThrowIfNull(page); + + float scale = dpi / PdfDpi; + int pixelWidth = Math.Max(1, (int)(region.Width * scale)); + int pixelHeight = Math.Max(1, (int)(region.Height * scale)); + + using var bitmap = new SKBitmap(pixelWidth, pixelHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.White); + + using var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true }; + + var fontCache = new Dictionary(); + try + { + foreach (var letter in page.Letters) + { + double lx = letter.Location.X; + double ly = letter.Location.Y; + + if (lx < region.Left || lx > region.Right || ly < region.Bottom || ly > region.Top) + { + continue; + } + + // Translate so region's bottom-left maps to image (0,0) + float x = (float)(lx - region.Left) * scale; + float y = (float)(region.Top - ly) * scale; + float fontSize = (float)letter.PointSize * scale; + if (fontSize < MinFontSize) + { + fontSize = DefaultFontScale * scale; + } + + if (!fontCache.TryGetValue(fontSize, out var font)) + { + font = new SKFont { Size = fontSize }; + fontCache[fontSize] = font; + } + + canvas.DrawText(letter.Value, x, y, SKTextAlign.Left, font, paint); + } + } + finally + { + foreach (var font in fontCache.Values) + { + font.Dispose(); + } + } + + using var image = SKImage.FromBitmap(bitmap); + using var data = image.Encode(SKEncodedImageFormat.Png, PngQuality); + return data.ToArray(); + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/PdfPig.DataIngestion.csproj b/MEDI/src/PdfPig.DataIngestion/PdfPig.DataIngestion.csproj new file mode 100644 index 0000000..26f6e6b --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PdfPig.DataIngestion.csproj @@ -0,0 +1,29 @@ + + + 1.0.0-preview.1 + CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion + $(AssemblyName) + net10.0;net8.0 + + PdfPig PDF reader for Microsoft.Extensions.DataIngestion pipelines + PDF document reader, vision LLM enrichers (OCR, table extraction, contextual chunking), and metadata-aware chunking for MEDI ingestion pipelines using PdfPig. + true + + + + + + + + + + + + + + + + + + + diff --git a/MEDI/src/PdfPig.DataIngestion/PdfPigDataIngestionServiceCollectionExtensions.cs b/MEDI/src/PdfPig.DataIngestion/PdfPigDataIngestionServiceCollectionExtensions.cs new file mode 100644 index 0000000..78ee2fb --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PdfPigDataIngestionServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +using System; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for configuring PdfPig data ingestion services. +/// +public static class PdfPigDataIngestionServiceCollectionExtensions +{ + /// + /// Adds a to the service collection. + /// + /// The service collection. + /// Optional delegate to configure reader options. + /// The service collection for chaining. + /// is . + public static IServiceCollection AddPdfPigReader( + this IServiceCollection services, + Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services); + + services.AddOptions(); + + if (configure is not null) + { + services.Configure(configure); + } + + services.TryAddSingleton(); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton, PdfPigReaderOptionsValidator>()); + + return services; + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/PdfPigReader.cs b/MEDI/src/PdfPig.DataIngestion/PdfPigReader.cs new file mode 100644 index 0000000..a0e0a1c --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PdfPigReader.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.Options; +using UglyToad.PdfPig; +using UglyToad.PdfPig.DocumentLayoutAnalysis; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; + +/// +/// Reads PDF documents using PdfPig and converts them to MEDI format. +/// Supports pluggable page segmentation via and configurable +/// reading modes via . +/// +public sealed class PdfPigReader : IngestionDocumentReader +{ + private readonly IPageSegmenter segmenter; + private readonly PdfReadingMode mode; + private readonly int renderDpi; + private readonly Func? elementTypeResolver; + + /// + /// Creates a new using dependency-injected options. + /// + /// The configured reader options. + /// + /// Page segmenter for layout analysis. Defaults to if . + /// + /// + /// Optional delegate that resolves the element type label from a . + /// + public PdfPigReader( + IOptions options, + IPageSegmenter? segmenter = null, + Func? elementTypeResolver = null) + { + ArgumentNullException.ThrowIfNull(options); + var opts = options.Value; + this.segmenter = segmenter ?? DefaultPageSegmenter.Instance; + this.mode = opts.Mode; + this.renderDpi = opts.RenderDpi; + this.elementTypeResolver = elementTypeResolver; + } + + /// + /// Creates a new . + /// + /// + /// Page segmenter for layout analysis. Defaults to if . + /// Ignored when is . + /// + /// + /// Controls the text extraction strategy. Defaults to . + /// + /// + /// The DPI to use when rendering page images. Applies to + /// and modes. Defaults to 150. + /// + /// + /// Optional delegate that resolves the element type label from a . + /// When provided, the resolved type is stored in element metadata as "element_type". + /// This decouples the reader from specific segmenter implementations (e.g. ONNX). + /// + public PdfPigReader( + IPageSegmenter? segmenter = null, + PdfReadingMode mode = PdfReadingMode.TextOnly, + int renderDpi = 150, + Func? elementTypeResolver = null) + { + this.segmenter = segmenter ?? DefaultPageSegmenter.Instance; + this.mode = mode; + this.renderDpi = renderDpi; + this.elementTypeResolver = elementTypeResolver; + } + + /// + public override Task ReadAsync( + Stream source, string identifier, string mediaType, CancellationToken cancellationToken = default) + { + using var pdfDocument = PdfDocument.Open(source); + var document = new IngestionDocument(identifier); + + for (var i = 1; i <= pdfDocument.NumberOfPages; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var page = pdfDocument.GetPage(i); + + var section = new IngestionDocumentSection + { + PageNumber = i + }; + + var renderImages = mode is PdfReadingMode.Hybrid or PdfReadingMode.VisionOnly; + + if (renderImages) + { + var imageBytes = PageImageRenderer.RenderPage(page, renderDpi); + section.Metadata["page_image"] = imageBytes; + section.Metadata["page_width"] = page.Width; + section.Metadata["page_height"] = page.Height; + } + + if (mode is not PdfReadingMode.VisionOnly) + { + var words = page.GetWords(); + var blocks = segmenter.GetBlocks(words); + + foreach (var block in blocks) + { + if (string.IsNullOrEmpty(block.Text)) + { + continue; + } + + var paragraph = new IngestionDocumentParagraph(block.Text) + { + Text = block.Text, + PageNumber = i + }; + + var bbox = block.BoundingBox; + paragraph.Metadata["BoundingBox.Left"] = bbox.Left; + paragraph.Metadata["BoundingBox.Bottom"] = bbox.Bottom; + paragraph.Metadata["BoundingBox.Right"] = bbox.Right; + paragraph.Metadata["BoundingBox.Top"] = bbox.Top; + + var elementType = elementTypeResolver?.Invoke(block); + if (elementType is not null) + { + paragraph.Metadata["element_type"] = elementType; + } + + section.Elements.Add(paragraph); + } + } + + // For scanned/image-only pages (or VisionOnly mode) with no elements, + // create a placeholder so VisionOcrEnricher can process the page image. + if (section.Elements.Count == 0 && renderImages) + { + var placeholder = new IngestionDocumentParagraph("[scanned-page]") + { + Text = string.Empty, + PageNumber = i + }; + placeholder.Metadata["placeholder"] = true; + section.Elements.Add(placeholder); + } + + document.Sections.Add(section); + } + + return Task.FromResult(document); + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptions.cs b/MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptions.cs new file mode 100644 index 0000000..5fc5973 --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptions.cs @@ -0,0 +1,18 @@ +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; + +/// +/// Options for configuring . +/// +public record PdfPigReaderOptions +{ + /// + /// Controls the text extraction strategy. Defaults to . + /// + public PdfReadingMode Mode { get; set; } = PdfReadingMode.TextOnly; + + /// + /// The DPI to use when rendering page images. Applies to + /// and modes. Defaults to 150. + /// + public int RenderDpi { get; set; } = 150; +} diff --git a/MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptionsValidator.cs b/MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptionsValidator.cs new file mode 100644 index 0000000..14b7fd3 --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PdfPigReaderOptionsValidator.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Options; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; + +/// +/// Validates at startup. +/// +internal sealed class PdfPigReaderOptionsValidator : IValidateOptions +{ + /// + public ValidateOptionsResult Validate(string? name, PdfPigReaderOptions options) + { + if (options.RenderDpi <= 0) + { + return ValidateOptionsResult.Fail("RenderDpi must be a positive integer."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/PdfReadingMode.cs b/MEDI/src/PdfPig.DataIngestion/PdfReadingMode.cs new file mode 100644 index 0000000..ac9ad57 --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/PdfReadingMode.cs @@ -0,0 +1,28 @@ +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; + +/// +/// Controls how extracts content from PDF pages. +/// +public enum PdfReadingMode +{ + /// + /// Native text extraction with page segmentation. No page images are rendered. + /// This is the fastest mode with zero external dependencies. + /// Scanned pages with no extractable text produce empty sections. + /// + TextOnly, + + /// + /// Native text extraction with page segmentation and page image rendering. + /// Scanned pages that produce no text receive placeholder elements for + /// downstream VLM-based OCR via VisionOcrEnricher. + /// + Hybrid, + + /// + /// Skip native text extraction entirely. Every page is rendered as an image + /// with a placeholder element. Requires a downstream VisionOcrEnricher + /// with a vision-capable LLM to populate the text content. + /// + VisionOnly +} diff --git a/MEDI/src/PdfPig.DataIngestion/Processors/ContextualChunkEnricher.cs b/MEDI/src/PdfPig.DataIngestion/Processors/ContextualChunkEnricher.cs new file mode 100644 index 0000000..df27e7a --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/Processors/ContextualChunkEnricher.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.Processors; + +/// +/// Enriches text chunks with contextual summaries for improved RAG retrieval. +/// Uses to generate a brief summary of each chunk. +/// +public sealed class ContextualChunkEnricher : IngestionChunkProcessor +{ + private readonly IChatClient chatClient; + + /// + /// Creates a new . + /// + /// The chat client used to generate contextual summaries. + /// is . + public ContextualChunkEnricher(IChatClient chatClient) + { + this.chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IAsyncEnumerable> chunks, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var chunk in chunks.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + var prompt = GetPromptForChunk(chunk); + + var messages = new[] + { + new ChatMessage(ChatRole.User, prompt) + }; + + var response = await chatClient.GetResponseAsync( + messages, + cancellationToken: cancellationToken).ConfigureAwait(false); + + chunk.Metadata["contextual_summary"] = response.Text; + + yield return chunk; + } + } + + private static string GetPromptForChunk(IngestionChunk chunk) + { + var elementType = chunk.Metadata.TryGetValue("element_type", out var t) ? t as string : null; + + var instruction = elementType switch + { + "table" => + "Summarize this table for search retrieval. Describe what data it contains, " + + "including key metrics, column headers, and notable values. " + + "Output only the summary sentence, nothing else.", + "picture" or "caption" => + "Summarize what this figure or image refers to for search retrieval. " + + "Output only the summary sentence, nothing else.", + _ => + "Provide a single concise sentence summarizing the following text for use " + + "in search retrieval. Output only the summary sentence, nothing else." + }; + + return instruction + "\n\n" + chunk.Content; + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/Processors/VisionOcrEnricher.cs b/MEDI/src/PdfPig.DataIngestion/Processors/VisionOcrEnricher.cs new file mode 100644 index 0000000..d05411c --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/Processors/VisionOcrEnricher.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.Processors; + +/// +/// Enriches document elements that have minimal or no text content by performing +/// vision LLM-based OCR, such as for scanned pages or image-heavy regions. +/// +public sealed class VisionOcrEnricher : IngestionDocumentProcessor +{ + private readonly IChatClient chatClient; + + /// + /// Creates a new . + /// + /// The chat client used to interact with a vision-capable LLM. + /// is . + public VisionOcrEnricher(IChatClient chatClient) + { + this.chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + IngestionDocument document, CancellationToken cancellationToken = default) + { + // Iterate by index so we can replace elements in-place. + // This is necessary because the MEDI chunker uses GetMarkdown() (which returns + // the immutable constructor parameter) rather than element.Text. Setting Text alone + // would leave stale placeholder content in GetMarkdown(), causing the chunker to + // produce chunks with placeholder text instead of the OCR'd content. + foreach (var section in document.Sections) + { + for (int i = 0; i < section.Elements.Count; i++) + { + var element = section.Elements[i]; + cancellationToken.ThrowIfCancellationRequested(); + + if (!string.IsNullOrWhiteSpace(element.Text)) + { + continue; + } + + // Try to get page image for vision-based OCR + byte[]? imageBytes = null; + if (element.PageNumber is int pageNumber) + { + foreach (var s in document.Sections) + { + if (s.PageNumber == pageNumber && + s.HasMetadata && + s.Metadata.TryGetValue("page_image", out var imageObj)) + { + imageBytes = imageObj as byte[]; + break; + } + } + } + + ChatMessage[] messages; + if (imageBytes is not null) + { + // Vision approach: send actual page image + messages = new[] + { + new ChatMessage(ChatRole.System, + "You are a precise OCR engine. Extract all visible text from the provided image exactly as it appears. " + + "Preserve line breaks and formatting. Output only the extracted text, no commentary."), + new ChatMessage(ChatRole.User, (IList)new AIContent[] + { + new DataContent(imageBytes, "image/png"), + new TextContent("Extract all text from this image.") + }) + }; + } + else + { + // Fallback: text-based approach when no image available + messages = new[] + { + new ChatMessage(ChatRole.User, + "You are an OCR engine. Extract all visible text from the following content. " + + "Return only the extracted text, preserving the original layout as much as possible.\n\n" + + (element.Text ?? string.Empty)) + }; + } + + var response = await chatClient.GetResponseAsync( + messages, + cancellationToken: cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(response.Text)) + { + // Replace the element with a new one whose markdown (constructor param) + // contains the OCR text, so downstream chunkers see it via GetMarkdown(). + var replacement = new IngestionDocumentParagraph(response.Text) + { + Text = response.Text, + PageNumber = element.PageNumber + }; + + if (element.HasMetadata) + { + foreach (var kv in element.Metadata) + { + replacement.Metadata[kv.Key] = kv.Value; + } + } + + replacement.Metadata["ocr_source"] = "vision_llm"; + section.Elements[i] = replacement; + } + } + } + + return document; + } +} diff --git a/MEDI/src/PdfPig.DataIngestion/Processors/VisionTableEnricher.cs b/MEDI/src/PdfPig.DataIngestion/Processors/VisionTableEnricher.cs new file mode 100644 index 0000000..e5e6b2e --- /dev/null +++ b/MEDI/src/PdfPig.DataIngestion/Processors/VisionTableEnricher.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.Processors; + +/// +/// Enriches table elements in a document by sending their content +/// to a vision-capable LLM via to extract markdown table content. +/// +public sealed class VisionTableEnricher : IngestionDocumentProcessor +{ + private readonly IChatClient chatClient; + + /// + /// Creates a new . + /// + /// The chat client used to interact with a vision-capable LLM. + /// is . + public VisionTableEnricher(IChatClient chatClient) + { + this.chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + IngestionDocument document, CancellationToken cancellationToken = default) + { + foreach (var element in document.EnumerateContent()) + { + if (element is not IngestionDocumentTable table) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Try to get page image for vision-based extraction + byte[]? imageBytes = null; + if (element.PageNumber is int pageNumber) + { + foreach (var section in document.Sections) + { + if (section.PageNumber == pageNumber && + section.HasMetadata && + section.Metadata.TryGetValue("page_image", out var imageObj)) + { + imageBytes = imageObj as byte[]; + break; + } + } + } + + ChatMessage[] messages; + if (imageBytes is not null) + { + // Vision approach: send actual page image + messages = new[] + { + new ChatMessage(ChatRole.System, + "You are a table extraction engine. Extract the table from the image as a markdown table. " + + "Use | as column separators. Include a header separator (| --- | --- |). " + + "Output ONLY the markdown table, nothing else."), + new ChatMessage(ChatRole.User, (IList)new AIContent[] + { + new DataContent(imageBytes, "image/png"), + new TextContent("Extract the table from this image as a markdown table.") + }) + }; + } + else + { + // Fallback: text-based extraction + messages = new[] + { + new ChatMessage(ChatRole.User, + "Extract the following table content into a well-formatted markdown table. " + + "Only output the markdown table, no other text.\n\n" + + (table.Text ?? string.Empty)) + }; + } + + var response = await chatClient.GetResponseAsync( + messages, + cancellationToken: cancellationToken).ConfigureAwait(false); + + table.Metadata["enriched_markdown_table"] = response.Text; + } + + return document; + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/AnnotatedTextBlock.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/AnnotatedTextBlock.cs new file mode 100644 index 0000000..48063fb --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/AnnotatedTextBlock.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// A that carries the layout detection label and confidence +/// from an ONNX model. Downstream code that only knows about +/// sees a regular block; callers that know about this subclass can read +/// and . +/// +public sealed class AnnotatedTextBlock : TextBlock +{ + /// + /// The element type label detected by the ONNX layout model + /// (e.g. "table", "picture", "section_header", "text"). + /// + public string Label { get; } + + /// + /// The model's confidence score for this detection, between 0 and 1. + /// + public float Confidence { get; } + + /// + /// Create a new . + /// + /// The text lines in this block. + /// The layout detection label. + /// The detection confidence score. + /// The separator used between lines. + public AnnotatedTextBlock( + IReadOnlyList lines, + string label, + float confidence, + string separator = "\n") + : base(lines, separator) + { + Label = label; + Confidence = confidence; + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/DetectionPostprocessing.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/DetectionPostprocessing.cs new file mode 100644 index 0000000..3c2c876 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/DetectionPostprocessing.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UglyToad.PdfPig.Core; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Static utility methods for postprocessing ONNX detection model outputs. +/// +public static class DetectionPostprocessing +{ + /// + /// Convert center-x, center-y, width, height (image coordinates, top-left origin) + /// to a (bottom-left origin, Y-up). + /// + /// Center X in image coordinates. + /// Center Y in image coordinates. + /// Width. + /// Height. + /// Page width for coordinate mapping. + /// Page height for coordinate mapping. + /// A PDF rectangle in PDF coordinate space. + public static PdfRectangle CxCyWhToRect(float cx, float cy, float w, float h, float pageWidth, float pageHeight) + { + float x1 = cx - w / 2f; + float y1 = cy - h / 2f; + float x2 = cx + w / 2f; + float y2 = cy + h / 2f; + + return XyxyToRect(x1, y1, x2, y2, pageWidth, pageHeight); + } + + /// + /// Convert x1, y1, x2, y2 (image coordinates, top-left origin, Y-down) + /// to a (bottom-left origin, Y-up). + /// + /// Left X in image coordinates. + /// Top Y in image coordinates. + /// Right X in image coordinates. + /// Bottom Y in image coordinates. + /// Page width for coordinate mapping. + /// Page height for coordinate mapping. + /// A PDF rectangle in PDF coordinate space. + public static PdfRectangle XyxyToRect(float x1, float y1, float x2, float y2, float pageWidth, float pageHeight) + { + // Image Y-axis is top-down; PDF Y-axis is bottom-up. + // Flip Y: pdfY = pageHeight - imageY + double pdfLeft = x1; + double pdfRight = x2; + double pdfBottom = pageHeight - y2; + double pdfTop = pageHeight - y1; + + return new PdfRectangle(pdfLeft, pdfBottom, pdfRight, pdfTop); + } + + /// + /// Apply Non-Maximum Suppression to remove overlapping detections. + /// Detections are processed in descending confidence order; a detection + /// is suppressed if its IoU with any already-kept detection exceeds the threshold. + /// + /// Input detections. + /// IoU threshold above which a detection is suppressed. + /// Filtered detections after NMS. + public static IReadOnlyList ApplyNms(IReadOnlyList detections, float iouThreshold = 0.45f) + { + ArgumentNullException.ThrowIfNull(detections); + + if (detections.Count <= 1) + { + return detections; + } + + var sorted = detections.OrderByDescending(d => d.Confidence).ToList(); + var kept = new List(); + var suppressed = new bool[sorted.Count]; + + for (int i = 0; i < sorted.Count; i++) + { + if (suppressed[i]) + { + continue; + } + + kept.Add(sorted[i]); + + for (int j = i + 1; j < sorted.Count; j++) + { + if (suppressed[j]) + { + continue; + } + + if (ComputeIoU(sorted[i].BoundingBox, sorted[j].BoundingBox) > iouThreshold) + { + suppressed[j] = true; + } + } + } + + return kept; + } + + /// + /// Scale detection bounding boxes from model coordinate space to PDF page coordinate space. + /// Handles optional letterbox padding removal. + /// + /// Detections in model coordinates. + /// Model input width. + /// Model input height. + /// Target page width (PDF units). + /// Target page height (PDF units). + /// Scale factor used during letterboxing, or null if not letterboxed. + /// Horizontal padding offset from letterboxing. + /// Vertical padding offset from letterboxing. + /// Detections with bounding boxes in page coordinate space. + public static IReadOnlyList ScaleToPage( + IReadOnlyList detections, + int modelWidth, + int modelHeight, + double pageWidth, + double pageHeight, + float? letterboxScale = null, + int padX = 0, + int padY = 0) + { + ArgumentNullException.ThrowIfNull(detections); + + var result = new List(detections.Count); + + foreach (var det in detections) + { + var box = det.BoundingBox; + + double left = box.Left; + double right = box.Right; + double bottom = box.Bottom; + double top = box.Top; + + if (letterboxScale.HasValue) + { + float s = letterboxScale.Value; + left = (left - padX) / s; + right = (right - padX) / s; + bottom = (bottom - padY) / s; + top = (top - padY) / s; + } + + // Scale from image pixel space to page space + double scaleX = pageWidth / modelWidth; + double scaleY = pageHeight / modelHeight; + + if (letterboxScale.HasValue) + { + // When letterboxed, coordinates were already unpadded/unscaled to original image size. + // Now scale from original image size to page size. + // The original image size = modelWidth / letterboxScale (approximately) + // but we already divided by letterboxScale, so coords are in original image space. + // We still need to map from original image dims to page dims. + // Since the original image was rendered from the page, they share the same aspect ratio. + scaleX = pageWidth / (modelWidth / letterboxScale.Value - padX * 2.0 / letterboxScale.Value); + scaleY = pageHeight / (modelHeight / letterboxScale.Value - padY * 2.0 / letterboxScale.Value); + } + + left *= scaleX; + right *= scaleX; + bottom *= scaleY; + top *= scaleY; + + // Clamp to page bounds + left = Math.Max(0, Math.Min(left, pageWidth)); + right = Math.Max(0, Math.Min(right, pageWidth)); + bottom = Math.Max(0, Math.Min(bottom, pageHeight)); + top = Math.Max(0, Math.Min(top, pageHeight)); + + var newBox = new PdfRectangle(left, bottom, right, top); + result.Add(det with { BoundingBox = newBox }); + } + + return result; + } + + /// + /// Compute the Intersection over Union (IoU) of two rectangles. + /// + /// First rectangle. + /// Second rectangle. + /// IoU value in [0, 1]. + public static float ComputeIoU(PdfRectangle a, PdfRectangle b) + { + double interLeft = Math.Max(a.Left, b.Left); + double interRight = Math.Min(a.Right, b.Right); + double interBottom = Math.Max(a.Bottom, b.Bottom); + double interTop = Math.Min(a.Top, b.Top); + + if (interLeft >= interRight || interBottom >= interTop) + { + return 0f; + } + + double interArea = (interRight - interLeft) * (interTop - interBottom); + double areaA = a.Width * a.Height; + double areaB = b.Width * b.Height; + double unionArea = areaA + areaB - interArea; + + if (unionArea <= 0) + { + return 0f; + } + + return (float)(interArea / unionArea); + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/ILayoutDetectionModel.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/ILayoutDetectionModel.cs new file mode 100644 index 0000000..f212885 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/ILayoutDetectionModel.cs @@ -0,0 +1,41 @@ +using Microsoft.ML.OnnxRuntime; +using SkiaSharp; +using System; +using System.Collections.Generic; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Strategy interface encapsulating model-specific preprocessing and postprocessing +/// for ONNX-based layout detection models. +/// +public interface ILayoutDetectionModel : IDisposable +{ + /// + /// Convert a page image to the model's expected input tensor(s). + /// + /// The rendered page image. + /// Original page image width in pixels. + /// Original page image height in pixels. + /// Named ONNX values ready for inference. + IReadOnlyList Preprocess(SKBitmap pageImage, int originalWidth, int originalHeight); + + /// + /// Parse model output into layout detections. + /// + /// The raw ONNX inference results. + /// Original page image width in pixels. + /// Original page image height in pixels. + /// Detected layout elements. + IReadOnlyList Postprocess(IDisposableReadOnlyCollection results, int originalWidth, int originalHeight); + + /// + /// Mapping from class ID to human-readable label name. + /// + IReadOnlyDictionary LabelMapping { get; } + + /// + /// Path to the ONNX model file on disk. + /// + string ModelPath { get; } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/ImagePreprocessing.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/ImagePreprocessing.cs new file mode 100644 index 0000000..68bd16b --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/ImagePreprocessing.cs @@ -0,0 +1,174 @@ +using Microsoft.ML.OnnxRuntime.Tensors; +using SkiaSharp; +using System; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Static utility methods for image preprocessing before ONNX model inference. +/// +public static class ImagePreprocessing +{ + /// + /// Resize an image maintaining aspect ratio and pad to the target size (letterboxing). + /// + /// Source image. + /// Target width. + /// Target height. + /// Color to use for padding. + /// The scale factor applied to the image. + /// Horizontal padding offset in pixels. + /// Vertical padding offset in pixels. + /// A new letterboxed bitmap. + public static SKBitmap Letterbox(SKBitmap image, int targetW, int targetH, SKColor padColor, out float scale, out int padX, out int padY) + { + ArgumentNullException.ThrowIfNull(image); + + float scaleX = (float)targetW / image.Width; + float scaleY = (float)targetH / image.Height; + scale = Math.Min(scaleX, scaleY); + + int newW = (int)(image.Width * scale); + int newH = (int)(image.Height * scale); + padX = (targetW - newW) / 2; + padY = (targetH - newH) / 2; + + var info = new SKImageInfo(targetW, targetH, SKColorType.Rgba8888, SKAlphaType.Premul); + var result = new SKBitmap(info); + + using var canvas = new SKCanvas(result); + canvas.Clear(padColor); + + var destRect = SKRect.Create(padX, padY, newW, newH); + using var skImage = SKImage.FromBitmap(image); + canvas.DrawImage(skImage, destRect, new SKSamplingOptions(SKCubicResampler.Mitchell)); + + return result; + } + + /// + /// Resize an image to exact dimensions, stretching if necessary. + /// + /// Source image. + /// Target width. + /// Target height. + /// A new resized bitmap. + public static SKBitmap ResizeExact(SKBitmap image, int targetW, int targetH) + { + ArgumentNullException.ThrowIfNull(image); + + var info = new SKImageInfo(targetW, targetH, SKColorType.Rgba8888, SKAlphaType.Premul); + var result = new SKBitmap(info); + + using var canvas = new SKCanvas(result); + var destRect = SKRect.Create(0, 0, targetW, targetH); + using var skImage = SKImage.FromBitmap(image); + canvas.DrawImage(skImage, destRect, new SKSamplingOptions(SKCubicResampler.Mitchell)); + + return result; + } + + /// + /// Extract a CHW byte tensor [1, 3, H, W] from an image. + /// Channel order is RGB. + /// + /// Source image (must be RGBA8888 or compatible). + /// A dense tensor with shape [1, 3, H, W]. + public static DenseTensor ToChwUint8(SKBitmap image) + { + ArgumentNullException.ThrowIfNull(image); + + int w = image.Width; + int h = image.Height; + var tensor = new DenseTensor([1, 3, h, w]); + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var pixel = image.GetPixel(x, y); + tensor[0, 0, y, x] = pixel.Red; + tensor[0, 1, y, x] = pixel.Green; + tensor[0, 2, y, x] = pixel.Blue; + } + } + + return tensor; + } + + /// + /// Extract a CHW float tensor [1, 3, H, W] from an image. + /// Values are normalized to [0, 1] range. Channel order is RGB. + /// + /// Source image (must be RGBA8888 or compatible). + /// A dense tensor with shape [1, 3, H, W]. + public static DenseTensor ToChwFloat(SKBitmap image) + { + ArgumentNullException.ThrowIfNull(image); + + int w = image.Width; + int h = image.Height; + var tensor = new DenseTensor([1, 3, h, w]); + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var pixel = image.GetPixel(x, y); + tensor[0, 0, y, x] = pixel.Red / 255f; + tensor[0, 1, y, x] = pixel.Green / 255f; + tensor[0, 2, y, x] = pixel.Blue / 255f; + } + } + + return tensor; + } + + /// + /// Apply ImageNet normalization in-place: mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]. + /// + /// The CHW float buffer (length must be 3 * width * height). + /// Image width. + /// Image height. + public static void NormalizeImageNet(Span chw, int width, int height) + { + ReadOnlySpan mean = [0.485f, 0.456f, 0.406f]; + ReadOnlySpan std = [0.229f, 0.224f, 0.225f]; + Normalize(chw, width, height, mean, std); + } + + /// + /// Apply custom per-channel normalization in-place: (value - mean) / std. + /// + /// The CHW float buffer (length must be 3 * width * height). + /// Image width. + /// Image height. + /// Per-channel mean values (length 3). + /// Per-channel standard deviation values (length 3). + public static void Normalize(Span chw, int width, int height, ReadOnlySpan mean, ReadOnlySpan std) + { + if (mean.Length != 3) + { + throw new ArgumentException("Mean must have exactly 3 elements.", nameof(mean)); + } + + if (std.Length != 3) + { + throw new ArgumentException("Std must have exactly 3 elements.", nameof(std)); + } + + int channelSize = width * height; + + for (int c = 0; c < 3; c++) + { + int offset = c * channelSize; + float m = mean[c]; + float s = std[c]; + + for (int i = 0; i < channelSize; i++) + { + chw[offset + i] = (chw[offset + i] - m) / s; + } + } + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/LayoutDetection.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/LayoutDetection.cs new file mode 100644 index 0000000..376f581 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/LayoutDetection.cs @@ -0,0 +1,12 @@ +using UglyToad.PdfPig.Core; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// A single detected layout element from an ONNX model. +/// +public record LayoutDetection( + PdfRectangle BoundingBox, + string Label, + int ClassId, + float Confidence); diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/ConfigurableLayoutModel.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/ConfigurableLayoutModel.cs new file mode 100644 index 0000000..eaa58ab --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/ConfigurableLayoutModel.cs @@ -0,0 +1,468 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using UglyToad.PdfPig.Core; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; + +/// +/// A configuration-driven layout detection model implementation. +/// Uses to determine preprocessing and postprocessing behavior. +/// +/// +/// +/// Thread safety: This type is not thread-safe. The method +/// stores letterbox state in instance fields that reads. +/// Concurrent Preprocess/Postprocess call pairs will produce incorrect results. +/// Use a separate instance per thread, or synchronize access externally. +/// +/// +public sealed class ConfigurableLayoutModel : ILayoutDetectionModel +{ + private readonly LayoutModelOptions _options; + private bool _disposed; + + // Letterbox state preserved between pre- and post-processing + private float _letterboxScale; + private int _padX; + private int _padY; + private bool _wasLetterboxed; + + /// + /// Create a new . + /// + /// Path to the ONNX model file. + /// Model configuration options. + public ConfigurableLayoutModel(string modelPath, LayoutModelOptions options) + { + ModelPath = modelPath ?? throw new ArgumentNullException(nameof(modelPath)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + public string ModelPath { get; } + + /// + public IReadOnlyDictionary LabelMapping => + _options.ClassLabels ?? new Dictionary(); + + /// + public IReadOnlyList Preprocess(SKBitmap pageImage, int originalWidth, int originalHeight) + { + ArgumentNullException.ThrowIfNull(pageImage); + + SKBitmap resized; + _wasLetterboxed = false; + + switch (_options.Resize) + { + case ResizeMode.Letterbox: + resized = ImagePreprocessing.Letterbox( + pageImage, + _options.InputWidth, + _options.InputHeight, + SKColors.Gray, + out _letterboxScale, + out _padX, + out _padY); + _wasLetterboxed = true; + break; + + case ResizeMode.AspectPreserve: + float scaleX = (float)_options.InputWidth / pageImage.Width; + float scaleY = (float)_options.InputHeight / pageImage.Height; + float scale = Math.Min(scaleX, scaleY); + int newW = (int)(pageImage.Width * scale); + int newH = (int)(pageImage.Height * scale); + resized = ImagePreprocessing.ResizeExact(pageImage, newW, newH); + break; + + default: // Exact + resized = ImagePreprocessing.ResizeExact(pageImage, _options.InputWidth, _options.InputHeight); + break; + } + + try + { + return _options.PixelFormat switch + { + PixelFormat.Float32Chw => CreateFloatInput(resized), + _ => CreateUint8Input(resized) + }; + } + finally + { + resized.Dispose(); + } + } + + private static List CreateUint8Input(SKBitmap image) + { + var tensor = ImagePreprocessing.ToChwUint8(image); + return new List + { + NamedOnnxValue.CreateFromTensor("images", tensor) + }; + } + + private List CreateFloatInput(SKBitmap image) + { + var tensor = ImagePreprocessing.ToChwFloat(image); + + if (_options.Normalization is not null) + { + var buffer = tensor.Buffer.Span; + ImagePreprocessing.Normalize( + buffer, + image.Width, + image.Height, + _options.Normalization.Mean.AsSpan(), + _options.Normalization.Std.AsSpan()); + } + + return new List + { + NamedOnnxValue.CreateFromTensor("images", tensor) + }; + } + + /// + public IReadOnlyList Postprocess( + IDisposableReadOnlyCollection results, + int originalWidth, + int originalHeight) + { + ArgumentNullException.ThrowIfNull(results); + + var detections = ParseOutputTensor(results, originalWidth, originalHeight); + + // Apply confidence filter + detections = detections + .Where(d => d.Confidence >= _options.ConfidenceThreshold) + .ToList(); + + // Apply NMS if required + if (_options.RequiresNms && detections.Count > 1) + { + detections = DetectionPostprocessing.ApplyNms(detections, _options.NmsIouThreshold).ToList(); + } + + // Scale to page coordinates if letterboxed + if (_wasLetterboxed) + { + detections = DetectionPostprocessing.ScaleToPage( + detections, + _options.InputWidth, + _options.InputHeight, + originalWidth, + originalHeight, + _letterboxScale, + _padX, + _padY).ToList(); + } + + return detections; + } + + private List ParseOutputTensor( + IDisposableReadOnlyCollection results, + int originalWidth, + int originalHeight) + { + var detections = new List(); + + // Try to find a single output tensor (common YOLO-style: [1, num_classes+4, num_detections]) + DisposableNamedOnnxValue? outputValue = null; + DisposableNamedOnnxValue? labelsValue = null; + DisposableNamedOnnxValue? boxesValue = null; + DisposableNamedOnnxValue? scoresValue = null; + + foreach (var result in results) + { + var name = result.Name; + if (name == "labels" || name == "pred_labels") + { + labelsValue = result; + } + else if (name == "boxes" || name == "pred_boxes") + { + boxesValue = result; + } + else if (name == "scores" || name == "pred_scores") + { + scoresValue = result; + } + else if (name == "output" || name == "output0") + { + outputValue = result; + } + } + + // If we have separate labels/boxes/scores outputs + if (labelsValue is not null && boxesValue is not null && scoresValue is not null) + { + return ParseSeparateOutputs(labelsValue, boxesValue, scoresValue, originalWidth, originalHeight); + } + + // Otherwise try a combined output tensor + if (outputValue?.Value is Tensor combinedTensor) + { + return ParseCombinedOutput(combinedTensor, originalWidth, originalHeight); + } + + // Fallback: try the first float tensor output + foreach (var result in results) + { + if (result.Value is Tensor tensor && tensor.Dimensions.Length >= 2) + { + return ParseCombinedOutput(tensor, originalWidth, originalHeight); + } + } + + return detections; + } + + private List ParseSeparateOutputs( + DisposableNamedOnnxValue labelsValue, + DisposableNamedOnnxValue boxesValue, + DisposableNamedOnnxValue scoresValue, + int originalWidth, + int originalHeight) + { + var detections = new List(); + + int[]? labels = ExtractIntArray(labelsValue); + float[]? scores = ExtractFloatArray(scoresValue); + float[]? boxes = ExtractBoxArray(boxesValue); + + if (labels is null || scores is null || boxes is null) + { + return detections; + } + + int count = labels.Length; + + for (int i = 0; i < count; i++) + { + int classId = labels[i]; + float confidence = scores[i]; + + float bx0 = boxes[i * 4 + 0]; + float bx1 = boxes[i * 4 + 1]; + float bx2 = boxes[i * 4 + 2]; + float bx3 = boxes[i * 4 + 3]; + + var rect = ConvertBbox(bx0, bx1, bx2, bx3, originalWidth, originalHeight); + string label = GetLabelName(classId); + + detections.Add(new LayoutDetection(rect, label, classId, confidence)); + } + + return detections; + } + + private List ParseCombinedOutput( + Tensor tensor, + int originalWidth, + int originalHeight) + { + var detections = new List(); + var dims = tensor.Dimensions; + + if (dims.Length == 3) + { + // Shape: [1, numDetections, 4+numClasses] or [1, 4+numClasses, numDetections] + int dim1 = dims[1]; + int dim2 = dims[2]; + + bool transposed = dim2 > dim1 && dim1 > 5; + + int numDetections; + int numValues; + + if (transposed) + { + // [1, 4+numClasses, numDetections] — need to transpose + numDetections = dim2; + numValues = dim1; + } + else + { + // [1, numDetections, 4+numClasses] + numDetections = dim1; + numValues = dim2; + } + + int numClasses = numValues - 4; + if (numClasses <= 0) + { + return detections; + } + + for (int i = 0; i < numDetections; i++) + { + float bx0, bx1, bx2, bx3; + int bestClass = 0; + float bestScore = float.MinValue; + + if (transposed) + { + bx0 = tensor[0, 0, i]; + bx1 = tensor[0, 1, i]; + bx2 = tensor[0, 2, i]; + bx3 = tensor[0, 3, i]; + + for (int c = 0; c < numClasses; c++) + { + float score = tensor[0, 4 + c, i]; + if (score > bestScore) + { + bestScore = score; + bestClass = c; + } + } + } + else + { + bx0 = tensor[0, i, 0]; + bx1 = tensor[0, i, 1]; + bx2 = tensor[0, i, 2]; + bx3 = tensor[0, i, 3]; + + for (int c = 0; c < numClasses; c++) + { + float score = tensor[0, i, 4 + c]; + if (score > bestScore) + { + bestScore = score; + bestClass = c; + } + } + } + + var rect = ConvertBbox(bx0, bx1, bx2, bx3, originalWidth, originalHeight); + string label = GetLabelName(bestClass); + + detections.Add(new LayoutDetection(rect, label, bestClass, bestScore)); + } + } + + return detections; + } + + private PdfRectangle ConvertBbox(float v0, float v1, float v2, float v3, int imageWidth, int imageHeight) + { + return _options.OutputBboxFormat switch + { + BboxFormat.CxCyWh => DetectionPostprocessing.CxCyWhToRect(v0, v1, v2, v3, imageWidth, imageHeight), + BboxFormat.Xywh => DetectionPostprocessing.XyxyToRect(v0, v1, v0 + v2, v1 + v3, imageWidth, imageHeight), + _ => DetectionPostprocessing.XyxyToRect(v0, v1, v2, v3, imageWidth, imageHeight) + }; + } + + private string GetLabelName(int classId) + { + if (_options.ClassLabels is not null && _options.ClassLabels.TryGetValue(classId, out var name)) + { + return name; + } + + return $"class_{classId}"; + } + + private static int[]? ExtractIntArray(DisposableNamedOnnxValue value) + { + if (value.Value is Tensor longTensor) + { + var dims = longTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var arr = new int[count]; + + for (int i = 0; i < count; i++) + { + arr[i] = dims.Length > 1 ? (int)longTensor[0, i] : (int)longTensor[i]; + } + + return arr; + } + + if (value.Value is Tensor intTensor) + { + var dims = intTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var arr = new int[count]; + + for (int i = 0; i < count; i++) + { + arr[i] = dims.Length > 1 ? intTensor[0, i] : intTensor[i]; + } + + return arr; + } + + return null; + } + + private static float[]? ExtractFloatArray(DisposableNamedOnnxValue value) + { + if (value.Value is Tensor floatTensor) + { + var dims = floatTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var arr = new float[count]; + + for (int i = 0; i < count; i++) + { + arr[i] = dims.Length > 1 ? floatTensor[0, i] : floatTensor[i]; + } + + return arr; + } + + return null; + } + + private static float[]? ExtractBoxArray(DisposableNamedOnnxValue value) + { + if (value.Value is Tensor floatTensor) + { + var dims = floatTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var arr = new float[count * 4]; + + for (int i = 0; i < count; i++) + { + if (dims.Length == 3) + { + arr[i * 4 + 0] = floatTensor[0, i, 0]; + arr[i * 4 + 1] = floatTensor[0, i, 1]; + arr[i * 4 + 2] = floatTensor[0, i, 2]; + arr[i * 4 + 3] = floatTensor[0, i, 3]; + } + else if (dims.Length == 2) + { + arr[i * 4 + 0] = floatTensor[i, 0]; + arr[i * 4 + 1] = floatTensor[i, 1]; + arr[i * 4 + 2] = floatTensor[i, 2]; + arr[i * 4 + 3] = floatTensor[i, 3]; + } + } + + return arr; + } + + return null; + } + + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + } + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/LayoutModelOptions.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/LayoutModelOptions.cs new file mode 100644 index 0000000..6ef3e1f --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/LayoutModelOptions.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; + +/// +/// Configuration options for a generic layout detection model, +/// driving the . +/// +public record LayoutModelOptions +{ + /// + /// Expected model input width in pixels. + /// + public int InputWidth { get; init; } = 640; + + /// + /// Expected model input height in pixels. + /// + public int InputHeight { get; init; } = 640; + + /// + /// How the image should be resized before feeding to the model. + /// + public ResizeMode Resize { get; init; } = ResizeMode.Exact; + + /// + /// Pixel format and data type for the input tensor. + /// + public PixelFormat PixelFormat { get; init; } = PixelFormat.Uint8Chw; + + /// + /// Optional per-channel normalization to apply after converting to float. + /// + public ImageNormalization? Normalization { get; init; } + + /// + /// Minimum confidence threshold for detections. + /// + public float ConfidenceThreshold { get; init; } = 0.3f; + + /// + /// IoU threshold for Non-Maximum Suppression. + /// + public float NmsIouThreshold { get; init; } = 0.45f; + + /// + /// Whether NMS should be applied to the output detections. + /// + public bool RequiresNms { get; init; } + + /// + /// Format of the bounding box coordinates in the model output. + /// + public BboxFormat OutputBboxFormat { get; init; } = BboxFormat.CxCyWh; + + /// + /// Mapping from class ID to human-readable label. If null, numeric labels are used. + /// + public IReadOnlyDictionary? ClassLabels { get; init; } +} + +/// +/// How the input image is resized before inference. +/// +public enum ResizeMode +{ + /// Stretch to exact target dimensions. + Exact, + + /// Preserve aspect ratio and pad with a solid color. + Letterbox, + + /// Preserve aspect ratio without padding (model must accept variable sizes). + AspectPreserve +} + +/// +/// Pixel format and data type for the model input tensor. +/// +public enum PixelFormat +{ + /// Unsigned 8-bit integer, CHW layout [1, 3, H, W]. + Uint8Chw, + + /// 32-bit float, CHW layout [1, 3, H, W], values in [0, 1]. + Float32Chw +} + +/// +/// Format of bounding box coordinates in the model output. +/// +public enum BboxFormat +{ + /// Center-X, Center-Y, Width, Height. + CxCyWh, + + /// Top-left X, top-left Y, bottom-right X, bottom-right Y. + Xyxy, + + /// Top-left X, top-left Y, Width, Height. + Xywh +} + +/// +/// Per-channel normalization parameters: (value - mean) / std. +/// +/// Per-channel mean values (RGB order, length 3). +/// Per-channel standard deviation values (RGB order, length 3). +public record ImageNormalization(float[] Mean, float[] Std); + +/// +/// Well-known normalization presets for common model architectures. +/// +public static class WellKnownNormalizations +{ + /// + /// ImageNet normalization: mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]. + /// + public static readonly ImageNormalization ImageNet = new([0.485f, 0.456f, 0.406f], [0.229f, 0.224f, 0.225f]); + + /// + /// Simple [0, 255] to [0, 1] normalization (mean=0, std=1/255). + /// + public static readonly ImageNormalization ZeroToOne = new([0f, 0f, 0f], [1f / 255f, 1f / 255f, 1f / 255f]); +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/RtDetrLayoutModel.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/RtDetrLayoutModel.cs new file mode 100644 index 0000000..faeac02 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/Models/RtDetrLayoutModel.cs @@ -0,0 +1,265 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using UglyToad.PdfPig.Core; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; + +/// +/// Layout detection model implementation for the Docling Layout Heron RT-DETR v2 model. +/// +/// +/// +/// RT-DETR (Real-Time DEtection TRansformer) uses built-in Hungarian matching, +/// so no external NMS is needed. +/// +/// +/// Input: Resize to 640×640 exact, uint8 CHW tensor (normalization is baked into the ONNX graph). +/// Also provides an orig_target_sizes int64 tensor with [height, width]. +/// +/// +/// Output: Handles both labels/boxes/scores and +/// pred_labels/pred_boxes/pred_scores naming conventions. +/// +/// +public sealed class RtDetrLayoutModel : ILayoutDetectionModel +{ + private const int ModelInputWidth = 640; + private const int ModelInputHeight = 640; + + private static readonly Dictionary DefaultLabelMapping = new Dictionary + { + [0] = "caption", + [1] = "footnote", + [2] = "formula", + [3] = "list_item", + [4] = "page_footer", + [5] = "page_header", + [6] = "picture", + [7] = "section_header", + [8] = "table", + [9] = "text", + [10] = "title", + [11] = "document_index", + [12] = "code", + [13] = "checkbox_selected", + [14] = "checkbox_unselected", + [15] = "form", + [16] = "key_value_region" + }; + + private bool _disposed; + + /// + /// Create a new . + /// + /// Path to the RT-DETR ONNX model file. + public RtDetrLayoutModel(string modelPath) + { + ModelPath = modelPath ?? throw new ArgumentNullException(nameof(modelPath)); + } + + /// + public string ModelPath { get; } + + /// + public IReadOnlyDictionary LabelMapping => DefaultLabelMapping; + + /// + public IReadOnlyList Preprocess(SKBitmap pageImage, int originalWidth, int originalHeight) + { + ArgumentNullException.ThrowIfNull(pageImage); + + // Resize to model input dimensions (exact, no letterbox) + using var resized = ImagePreprocessing.ResizeExact(pageImage, ModelInputWidth, ModelInputHeight); + + // Convert to CHW uint8 tensor — normalization is baked into the ONNX graph + var imageTensor = ImagePreprocessing.ToChwUint8(resized); + + // Create orig_target_sizes tensor [1, 2] with [height, width] + var origSizesTensor = new DenseTensor([1, 2]); + origSizesTensor[0, 0] = originalHeight; + origSizesTensor[0, 1] = originalWidth; + + return new List + { + NamedOnnxValue.CreateFromTensor("images", imageTensor), + NamedOnnxValue.CreateFromTensor("orig_target_sizes", origSizesTensor) + }; + } + + /// + public IReadOnlyList Postprocess( + IDisposableReadOnlyCollection results, + int originalWidth, + int originalHeight) + { + ArgumentNullException.ThrowIfNull(results); + + // Try both naming conventions + var labelsValue = TryGetOutput(results, "labels") ?? TryGetOutput(results, "pred_labels"); + var boxesValue = TryGetOutput(results, "boxes") ?? TryGetOutput(results, "pred_boxes"); + var scoresValue = TryGetOutput(results, "scores") ?? TryGetOutput(results, "pred_scores"); + + if (labelsValue is null || boxesValue is null || scoresValue is null) + { + return Array.Empty(); + } + + var labels = ExtractLabels(labelsValue); + var scores = ExtractScores(scoresValue); + var boxes = ExtractBoxes(boxesValue); + + if (labels is null || scores is null || boxes is null) + { + return Array.Empty(); + } + + int count = labels.Length; + var detections = new List(count); + + for (int i = 0; i < count; i++) + { + int classId = labels[i]; + float confidence = scores[i]; + + // Boxes are in absolute pixel coordinates (post orig_target_sizes scaling) + float x1 = boxes[i * 4 + 0]; + float y1 = boxes[i * 4 + 1]; + float x2 = boxes[i * 4 + 2]; + float y2 = boxes[i * 4 + 3]; + + string label = DefaultLabelMapping.TryGetValue(classId, out var name) + ? name + : $"class_{classId}"; + + // Convert from image coords (top-left origin) to PDF coords (bottom-left origin) + var rect = DetectionPostprocessing.XyxyToRect(x1, y1, x2, y2, originalWidth, originalHeight); + + detections.Add(new LayoutDetection(rect, label, classId, confidence)); + } + + return detections; + } + + private static DisposableNamedOnnxValue? TryGetOutput( + IDisposableReadOnlyCollection results, + string name) + { + foreach (var result in results) + { + if (string.Equals(result.Name, name, StringComparison.Ordinal)) + { + return result; + } + } + + return null; + } + + private static int[]? ExtractLabels(DisposableNamedOnnxValue value) + { + // Try int64 tensor first (most common) + if (value.Value is Tensor longTensor) + { + var dims = longTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var labels = new int[count]; + + for (int i = 0; i < count; i++) + { + labels[i] = dims.Length > 1 + ? (int)longTensor[0, i] + : (int)longTensor[i]; + } + + return labels; + } + + // Try int32 tensor + if (value.Value is Tensor intTensor) + { + var dims = intTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var labels = new int[count]; + + for (int i = 0; i < count; i++) + { + labels[i] = dims.Length > 1 + ? intTensor[0, i] + : intTensor[i]; + } + + return labels; + } + + return null; + } + + private static float[]? ExtractScores(DisposableNamedOnnxValue value) + { + if (value.Value is Tensor floatTensor) + { + var dims = floatTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var scores = new float[count]; + + for (int i = 0; i < count; i++) + { + scores[i] = dims.Length > 1 + ? floatTensor[0, i] + : floatTensor[i]; + } + + return scores; + } + + return null; + } + + private static float[]? ExtractBoxes(DisposableNamedOnnxValue value) + { + if (value.Value is Tensor floatTensor) + { + var dims = floatTensor.Dimensions; + int count = dims.Length > 1 ? dims[1] : dims[0]; + var boxes = new float[count * 4]; + + for (int i = 0; i < count; i++) + { + if (dims.Length == 3) + { + // Shape: [batch, num_detections, 4] + boxes[i * 4 + 0] = floatTensor[0, i, 0]; + boxes[i * 4 + 1] = floatTensor[0, i, 1]; + boxes[i * 4 + 2] = floatTensor[0, i, 2]; + boxes[i * 4 + 3] = floatTensor[0, i, 3]; + } + else if (dims.Length == 2) + { + // Shape: [num_detections, 4] + boxes[i * 4 + 0] = floatTensor[i, 0]; + boxes[i * 4 + 1] = floatTensor[i, 1]; + boxes[i * 4 + 2] = floatTensor[i, 2]; + boxes[i * 4 + 3] = floatTensor[i, 3]; + } + } + + return boxes; + } + + return null; + } + + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + } + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenter.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenter.cs new file mode 100644 index 0000000..8565a16 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenter.cs @@ -0,0 +1,293 @@ +using Microsoft.Extensions.Options; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.Core; +using UglyToad.PdfPig.DocumentLayoutAnalysis; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Page segmenter that uses an ONNX layout detection model to identify +/// document regions and assign words to detected blocks. +/// +public class OnnxPageSegmenter : IPageSegmenter, IDisposable, IAsyncDisposable +{ + private readonly ILayoutDetectionModel _model; + private readonly Microsoft.ML.OnnxRuntime.InferenceSession _session; + private readonly float _confidenceThreshold; + private readonly int _renderDpi; + private bool _disposed; + + /// + /// Create a new using dependency-injected options. + /// + /// The layout detection model to use. + /// The configured options. + public OnnxPageSegmenter(ILayoutDetectionModel model, IOptions options) + : this(model, GetOptionsValue(options)) + { + } + + /// + /// Create a new . + /// + /// The layout detection model to use. + /// Optional segmenter configuration. + public OnnxPageSegmenter(ILayoutDetectionModel model, OnnxSegmenterOptions? options = null) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _confidenceThreshold = options?.ConfidenceThreshold ?? 0.3f; + _renderDpi = options?.RenderDpi ?? 150; + var sessionOpts = options?.SessionOptions ?? new Microsoft.ML.OnnxRuntime.SessionOptions(); + _session = new Microsoft.ML.OnnxRuntime.InferenceSession(model.ModelPath, sessionOpts); + } + + /// + /// Get text blocks by running ONNX layout detection and assigning words to detected regions. + /// + /// The page's words to generate text blocks for. + /// A list of text blocks from this approach. + public IReadOnlyList GetBlocks(IEnumerable words) + { + ObjectDisposedException.ThrowIf(_disposed, this); + var wordList = words?.ToList() ?? throw new ArgumentNullException(nameof(words)); + if (wordList.Count == 0) + { + return Array.Empty(); + } + + // 1. Compute page bounds from words + double minX = double.MaxValue, minY = double.MaxValue; + double maxX = double.MinValue, maxY = double.MinValue; + foreach (var word in wordList) + { + var bb = word.BoundingBox; + minX = Math.Min(minX, bb.Left); + minY = Math.Min(minY, bb.Bottom); + maxX = Math.Max(maxX, bb.Right); + maxY = Math.Max(maxY, bb.Top); + } + + double pageWidth = maxX - minX; + double pageHeight = maxY - minY; + + if (pageWidth <= 0 || pageHeight <= 0) + { + return [new TextBlock(CreateSingleLine(wordList))]; + } + + // 2. Render page image using PageImageRenderer + using SKBitmap pageImage = PageImageRenderer.RenderWords(wordList, pageWidth, pageHeight, _renderDpi); + int imageWidth = pageImage.Width; + int imageHeight = pageImage.Height; + + // 3. Preprocess with model + var inputs = _model.Preprocess(pageImage, imageWidth, imageHeight); + + // 4. Run inference + using var results = _session.Run(inputs); + + // 5. Postprocess with model + var detections = _model.Postprocess(results, imageWidth, imageHeight); + + // 6. Filter by confidence + var filtered = new List(); + foreach (var det in detections) + { + if (det.Confidence >= _confidenceThreshold) + { + filtered.Add(det); + } + } + + if (filtered.Count == 0) + { + return [new TextBlock(CreateSingleLine(wordList))]; + } + + // 7. Map detections to TextBlocks via bbox overlap + return MapDetectionsToBlocks(filtered, wordList, pageWidth, pageHeight, minX, minY); + } + + private static List MapDetectionsToBlocks( + List detections, + List words, + double pageWidth, + double pageHeight, + double offsetX, + double offsetY) + { + var wordAssigned = new bool[words.Count]; + var blocks = new List(); + + foreach (var detection in detections) + { + var capturedWords = new List(); + + for (int i = 0; i < words.Count; i++) + { + if (wordAssigned[i]) + { + continue; + } + + if (HasOverlap(detection.BoundingBox, words[i].BoundingBox)) + { + capturedWords.Add(words[i]); + wordAssigned[i] = true; + } + } + + if (capturedWords.Count > 0) + { + var lines = GroupWordsIntoLines(capturedWords); + blocks.Add(new AnnotatedTextBlock(lines, detection.Label, detection.Confidence)); + } + } + + // 8. Handle uncaptured words — group into a fallback block + var uncaptured = new List(); + for (int i = 0; i < words.Count; i++) + { + if (!wordAssigned[i]) + { + uncaptured.Add(words[i]); + } + } + + if (uncaptured.Count > 0) + { + var lines = GroupWordsIntoLines(uncaptured); + blocks.Add(new TextBlock(lines)); + } + + return blocks; + } + + private static bool HasOverlap(PdfRectangle a, PdfRectangle b) + { + double interLeft = Math.Max(a.Left, b.Left); + double interRight = Math.Min(a.Right, b.Right); + double interBottom = Math.Max(a.Bottom, b.Bottom); + double interTop = Math.Min(a.Top, b.Top); + + return interLeft < interRight && interBottom < interTop; + } + + private static IReadOnlyList GroupWordsIntoLines(List words) + { + if (words.Count == 0) + { + return Array.Empty(); + } + + // Sort words by vertical position (top to bottom), then left to right + words.Sort((a, b) => + { + double aY = a.BoundingBox.Top; + double bY = b.BoundingBox.Top; + + // Group by Y-proximity: if vertical centers are close, treat as same line + double aCenter = (a.BoundingBox.Top + a.BoundingBox.Bottom) / 2.0; + double bCenter = (b.BoundingBox.Top + b.BoundingBox.Bottom) / 2.0; + double aHeight = a.BoundingBox.Height; + double bHeight = b.BoundingBox.Height; + double tolerance = Math.Min(aHeight, bHeight) * 0.5; + + if (Math.Abs(aCenter - bCenter) <= tolerance) + { + return a.BoundingBox.Left.CompareTo(b.BoundingBox.Left); + } + + // Higher Y = higher on page in PDF coords, so sort descending + return bY.CompareTo(aY); + }); + + var lines = new List(); + var currentLineWords = new List { words[0] }; + + for (int i = 1; i < words.Count; i++) + { + var prev = currentLineWords[^1]; + var curr = words[i]; + + double prevCenter = (prev.BoundingBox.Top + prev.BoundingBox.Bottom) / 2.0; + double currCenter = (curr.BoundingBox.Top + curr.BoundingBox.Bottom) / 2.0; + double tolerance = Math.Min(prev.BoundingBox.Height, curr.BoundingBox.Height) * 0.5; + + if (Math.Abs(prevCenter - currCenter) <= tolerance) + { + currentLineWords.Add(curr); + } + else + { + // Sort current line words left to right before creating line + currentLineWords.Sort((a, b) => a.BoundingBox.Left.CompareTo(b.BoundingBox.Left)); + lines.Add(new TextLine(currentLineWords.ToList())); + currentLineWords.Clear(); + currentLineWords.Add(curr); + } + } + + if (currentLineWords.Count > 0) + { + currentLineWords.Sort((a, b) => a.BoundingBox.Left.CompareTo(b.BoundingBox.Left)); + lines.Add(new TextLine(currentLineWords.ToList())); + } + + return lines; + } + + private static IReadOnlyList CreateSingleLine(List words) + { + return [new TextLine(words)]; + } + + private static OnnxSegmenterOptions GetOptionsValue(IOptions options) + { + ArgumentNullException.ThrowIfNull(options); + return options.Value; + } + + /// + /// Dispose resources held by this segmenter. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Asynchronously dispose resources held by this segmenter. + /// + public ValueTask DisposeAsync() + { + Dispose(true); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + + /// + /// Dispose managed and unmanaged resources. + /// + /// Whether managed resources should be disposed. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _session.Dispose(); + _model.Dispose(); + } + + _disposed = true; + } + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenterServiceCollectionExtensions.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenterServiceCollectionExtensions.cs new file mode 100644 index 0000000..fd78e2a --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxPageSegmenterServiceCollectionExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for configuring ONNX-based page segmentation services. +/// +public static class OnnxPageSegmenterServiceCollectionExtensions +{ + /// + /// Adds an and its dependencies to the service collection. + /// + /// + /// The implementation to use (e.g. + /// or ). + /// + /// The service collection. + /// Optional delegate to configure . + /// The service collection for chaining. + /// is . + public static IServiceCollection AddOnnxPageSegmenter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TModel>( + this IServiceCollection services, + Action? configure = null) + where TModel : class, ILayoutDetectionModel + { + ArgumentNullException.ThrowIfNull(services); + + services.AddOptions(); + + if (configure is not null) + { + services.Configure(configure); + } + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => sp.GetRequiredService()); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton, OnnxSegmenterOptionsValidator>()); + + return services; + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptions.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptions.cs new file mode 100644 index 0000000..620cb7d --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptions.cs @@ -0,0 +1,24 @@ +using Microsoft.ML.OnnxRuntime; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Options for configuring the . +/// +public record OnnxSegmenterOptions +{ + /// + /// Minimum confidence threshold for detections (0.0 to 1.0). + /// + public float ConfidenceThreshold { get; set; } = 0.3f; + + /// + /// ONNX Runtime session options. Use to configure GPU, thread count, etc. + /// + public SessionOptions? SessionOptions { get; set; } + + /// + /// DPI for rendering the page image. Higher values improve accuracy but are slower. + /// + public int RenderDpi { get; set; } = 150; +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptionsValidator.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptionsValidator.cs new file mode 100644 index 0000000..041df1c --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/OnnxSegmenterOptionsValidator.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Options; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Validates at startup to catch configuration errors early. +/// +internal sealed class OnnxSegmenterOptionsValidator : IValidateOptions +{ + /// + public ValidateOptionsResult Validate(string? name, OnnxSegmenterOptions options) + { + if (options.ConfidenceThreshold is < 0f or > 1f) + { + return ValidateOptionsResult.Fail("ConfidenceThreshold must be between 0.0 and 1.0."); + } + + if (options.RenderDpi <= 0) + { + return ValidateOptionsResult.Fail("RenderDpi must be a positive integer."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/PageImageRenderer.cs b/MEDI/src/PdfPig.OnnxLayoutAnalysis/PageImageRenderer.cs new file mode 100644 index 0000000..55e8d02 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/PageImageRenderer.cs @@ -0,0 +1,77 @@ +using SkiaSharp; +using System; +using System.Collections.Generic; +using UglyToad.PdfPig.Content; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; + +/// +/// Renders word bounding boxes to an for use as +/// input to ONNX layout detection models. +/// +public static class PageImageRenderer +{ + /// + /// Render word bounding boxes as filled rectangles on a white background. + /// PDF coordinates (bottom-left origin, Y-up) are converted to image coordinates + /// (top-left origin, Y-down). + /// + /// The words whose bounding boxes to render. + /// The page width in PDF units. + /// The page height in PDF units. + /// Rendering DPI. Higher values produce larger images with more detail. + /// A new bitmap with word bounding boxes rendered. + public static SKBitmap RenderWords(IReadOnlyList words, double pageWidth, double pageHeight, int dpi = 150) + { + ArgumentNullException.ThrowIfNull(words); + + // Scale from PDF points (72 dpi) to target DPI + double scale = dpi / 72.0; + int imageWidth = Math.Max(1, (int)(pageWidth * scale)); + int imageHeight = Math.Max(1, (int)(pageHeight * scale)); + + var info = new SKImageInfo(imageWidth, imageHeight, SKColorType.Rgba8888, SKAlphaType.Premul); + var bitmap = new SKBitmap(info); + + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.White); + + using var paint = new SKPaint(); + paint.IsAntialias = false; + paint.Color = SKColors.Black; + paint.Style = SKPaintStyle.Fill; + + // Compute the bounds offset so we render relative to (0, 0) + double minX = double.MaxValue; + double minY = double.MaxValue; + foreach (var word in words) + { + minX = Math.Min(minX, word.BoundingBox.Left); + minY = Math.Min(minY, word.BoundingBox.Bottom); + } + + foreach (var word in words) + { + var bb = word.BoundingBox; + + // Translate to origin + double left = (bb.Left - minX) * scale; + double right = (bb.Right - minX) * scale; + double pdfBottom = (bb.Bottom - minY) * scale; + double pdfTop = (bb.Top - minY) * scale; + + // Convert PDF Y (bottom-up) to image Y (top-down) + float imgLeft = (float)left; + float imgTop = (float)(imageHeight - pdfTop); + float imgRight = (float)right; + float imgBottom = (float)(imageHeight - pdfBottom); + + if (imgRight > imgLeft && imgBottom > imgTop) + { + canvas.DrawRect(new SKRect(imgLeft, imgTop, imgRight, imgBottom), paint); + } + } + + return bitmap; + } +} diff --git a/MEDI/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj b/MEDI/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj new file mode 100644 index 0000000..182f186 --- /dev/null +++ b/MEDI/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj @@ -0,0 +1,27 @@ + + + 1.0.0-preview.1 + CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis + $(AssemblyName) + net10.0;net8.0 + + ONNX-based layout detection for PdfPig document processing + ONNX Runtime layout detection models (RT-DETR, YOLO, configurable) that implement PdfPig's IPageSegmenter for intelligent document layout analysis. + true + + + + + + + + + + + + + + + + + diff --git a/MEDI/test/Directory.Build.props b/MEDI/test/Directory.Build.props new file mode 100644 index 0000000..2cf1445 --- /dev/null +++ b/MEDI/test/Directory.Build.props @@ -0,0 +1,16 @@ + + + + + + $(NoWarn);CA1515 + $(NoWarn);CA1707 + $(NoWarn);CA1716 + $(NoWarn);CA1720 + $(NoWarn);CA1819 + $(NoWarn);CA1861 + $(NoWarn);CA2007;VSTHRD111 + $(NoWarn);CS1591 + + + diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/ContextualChunkEnricherTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/ContextualChunkEnricherTests.cs new file mode 100644 index 0000000..c5a1110 --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/ContextualChunkEnricherTests.cs @@ -0,0 +1,197 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.Processors; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class ContextualChunkEnricherTests +{ + private class TestChatClient : IChatClient + { + private readonly string _response; + + public TestChatClient(string response) => _response = response; + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var msg = new ChatMessage(ChatRole.Assistant, _response); + return Task.FromResult(new ChatResponse(msg)); + } + + public IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() { } + } + + [Fact] + public void Constructor_NullChatClient_ThrowsArgumentNullException() + { + Assert.Throws(() => new ContextualChunkEnricher(null!)); + } + + [Fact] + public async Task ProcessAsync_AddsContextualSummaryMetadata() + { + var summaryText = "This chunk discusses PDF text extraction."; + var client = new TestChatClient(summaryText); + var enricher = new ContextualChunkEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var chunk = new IngestionChunk("Some text about PDF extraction and analysis.", doc, "Page 1"); + + var results = new List>(); + await foreach (var c in enricher.ProcessAsync(ToAsyncEnumerable(chunk))) + { + results.Add(c); + } + + Assert.Single(results); + Assert.True(results[0].Metadata.ContainsKey("contextual_summary")); + Assert.Equal(summaryText, results[0].Metadata["contextual_summary"]); + } + + [Fact] + public async Task ProcessAsync_MultipleChunks_AllGetSummaries() + { + var summaryText = "Summary text."; + var client = new TestChatClient(summaryText); + var enricher = new ContextualChunkEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var chunk1 = new IngestionChunk("First chunk content.", doc, "Page 1"); + var chunk2 = new IngestionChunk("Second chunk content.", doc, "Page 2"); + + var results = new List>(); + await foreach (var c in enricher.ProcessAsync(ToAsyncEnumerable(chunk1, chunk2))) + { + results.Add(c); + } + + Assert.Equal(2, results.Count); + Assert.All(results, r => + { + Assert.True(r.Metadata.ContainsKey("contextual_summary")); + Assert.Equal(summaryText, r.Metadata["contextual_summary"]); + }); + } + + [Fact] + public async Task ProcessAsync_EmptyChunks_NoResults() + { + var client = new TestChatClient("summary"); + var enricher = new ContextualChunkEnricher(client); + + var results = new List>(); + await foreach (var c in enricher.ProcessAsync(ToAsyncEnumerable>())) + { + results.Add(c); + } + + Assert.Empty(results); + } + + [Fact] + public async Task ProcessAsync_CancellationRequested_Throws() + { + var client = new TestChatClient("summary"); + var enricher = new ContextualChunkEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var chunk = new IngestionChunk("Some content.", doc, "Page 1"); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var _ in enricher.ProcessAsync(ToAsyncEnumerable(chunk), cts.Token)) + { + } + }); + } + + [Fact] + public async Task ProcessAsync_PreservesChunkContent() + { + var client = new TestChatClient("A summary."); + var enricher = new ContextualChunkEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var originalContent = "Original chunk content stays the same."; + var chunk = new IngestionChunk(originalContent, doc, "Page 1"); + + var results = new List>(); + await foreach (var c in enricher.ProcessAsync(ToAsyncEnumerable(chunk))) + { + results.Add(c); + } + + Assert.Single(results); + Assert.Equal(originalContent, results[0].Content); + } + + [Fact] + public async Task ProcessAsync_PreservesChunkDocumentReference() + { + var client = new TestChatClient("A summary."); + var enricher = new ContextualChunkEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var chunk = new IngestionChunk("Content.", doc, "Page 1"); + + var results = new List>(); + await foreach (var c in enricher.ProcessAsync(ToAsyncEnumerable(chunk))) + { + results.Add(c); + } + + Assert.Single(results); + Assert.Same(doc, results[0].Document); + } + + [Fact] + public async Task ProcessAsync_YieldsSameChunkInstances() + { + var client = new TestChatClient("Summary."); + var enricher = new ContextualChunkEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var chunk = new IngestionChunk("Content.", doc, "Page 1"); + + var results = new List>(); + await foreach (var c in enricher.ProcessAsync(ToAsyncEnumerable(chunk))) + { + results.Add(c); + } + + Assert.Single(results); + Assert.Same(chunk, results[0]); + } + + private static async IAsyncEnumerable ToAsyncEnumerable(params T[] items) + { + foreach (var item in items) + { + await Task.Yield(); + yield return item; + } + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/cat-genetics.pdf b/MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/cat-genetics.pdf new file mode 100644 index 0000000000000000000000000000000000000000..90c7afe8bb8415bdfca91e76417f8f84fa47e75c GIT binary patch literal 516459 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�luc2Y}!QGQkf+Qj8i94xHdYC%DN3 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%0AvHdkXRnuB2Z!p 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 z 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 literal 0 HcmV?d00001 diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/data.pdf b/MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/data.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8bdda7f411ecd3958f53e62e74d84afc02367b93 GIT binary patch literal 28330 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 sFDVV=<&u<^~HKaBc_LQgNFCX4d_09KU8_W%F@ literal 0 HcmV?d00001 diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/inherited_mediabox.pdf b/MEDI/test/PdfPig.DataIngestion.UnitTests/Documents/inherited_mediabox.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2d4468c4dc4afd1c461c6576dd293df1f39ec16d GIT binary patch literal 1462 zcmbtU-*4J55PtVxaW803G&V^XRH)Jl>sIYY5meeF#Dhs3NCg-LgXs2QXQH9@dTp1{ST9XQ9h^=sE`-_U3}W*s zIDn#|-ETdq=u??jvzTwpqc0{e^`$+~Ov2HTl9ZJ56BEN~CP8&z=E8ogodH`240B>C zyLTC+3*||gDyPa8{x9Jxh4mq`V)XowqD({Tmh@!}QsEL$P z$wVjJq@Q$yfeM5tA7B8tkTQv}jb-O?9``Z4JQE?iGI^XmSY4T7^<__LOn&w;wajFJ zrR72zEcWF}$sAUp5L3Zmlu#WNV@VY^2PZA+7?mS~m;?84jp-JFgo&O_@!au0c2Q(f zphODM>q@bvpj~$0g~h*sB{;@{=kEjmhN!_n{tJv!1Z>*m_zE=?V!W08qmH2L74XrG ze_UgqaXxr*Hk^x$3w#@~hLI*YKz(jF&D+$p9{!Y@*3-PqD#0t-fKoIVo~TB6W*hOO zJ&f#2zJoJ`kkqa72m+~_rg2IIoqRp2PqrstiYCH?PCnIniR&cY=Q!1o=(fkZ-f1)m g$BlPlE&3;fPogP*XWfvPc0Py%yArL|` +/// Helper to locate test PDF documents for integration tests. +/// Looks in a "Documents" folder relative to the test output directory. +/// +internal static class IntegrationHelpers +{ + private static readonly string DocumentsFolder = Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, "Documents"); + + public static string GetDocumentPath(string name) + { + // Try with .pdf extension first + var path = Path.Combine(DocumentsFolder, name + ".pdf"); + if (File.Exists(path)) + { + return path; + } + + // Try without extension + path = Path.Combine(DocumentsFolder, name); + if (File.Exists(path)) + { + return path; + } + + // Return the .pdf path (caller's test will fail with a clear "file not found" message) + return Path.Combine(DocumentsFolder, name + ".pdf"); + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/MetadataAwareSectionChunkerTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/MetadataAwareSectionChunkerTests.cs new file mode 100644 index 0000000..a9040c5 --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/MetadataAwareSectionChunkerTests.cs @@ -0,0 +1,179 @@ +#if NET8_0_OR_GREATER +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.DataIngestion.Chunkers; +using Microsoft.ML.Tokenizers; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class MetadataAwareSectionChunkerTests +{ + private static MetadataAwareSectionChunker CreateChunker() + { + var tokenizer = TiktokenTokenizer.CreateForModel("gpt-4o"); + var options = new IngestionChunkerOptions(tokenizer) { MaxTokensPerChunk = 10000 }; + return new MetadataAwareSectionChunker(options); + } + + private static async Task>> CollectChunksAsync( + MetadataAwareSectionChunker chunker, + IngestionDocument doc) + { + var chunks = new List>(); + await foreach (var chunk in chunker.ProcessAsync(doc)) + { + chunks.Add(chunk); + } + return chunks; + } + + [Fact] + public async Task ProcessAsync_PropagatesElementTypeMetadata() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var paragraph = new IngestionDocumentParagraph("Some text") { Text = "Some text" }; + paragraph.Metadata["element_type"] = "text"; + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.True(chunks[0].Metadata.ContainsKey("element_type")); + Assert.Equal("text", chunks[0].Metadata["element_type"]); + } + + [Fact] + public async Task ProcessAsync_SkipsBoundingBoxMetadata() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var paragraph = new IngestionDocumentParagraph("Heading text") { Text = "Heading text" }; + paragraph.Metadata["BoundingBox.Left"] = 10.0; + paragraph.Metadata["element_type"] = "heading"; + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.True(chunks[0].Metadata.ContainsKey("element_type")); + Assert.Equal("heading", chunks[0].Metadata["element_type"]); + Assert.False(chunks[0].Metadata.ContainsKey("BoundingBox.Left")); + } + + [Fact] + public async Task ProcessAsync_FirstMatchWinsForConflictingKeys() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + + var paragraph1 = new IngestionDocumentParagraph("First paragraph") { Text = "First paragraph" }; + paragraph1.Metadata["element_type"] = "text"; + section.Elements.Add(paragraph1); + + var paragraph2 = new IngestionDocumentParagraph("Second paragraph") { Text = "Second paragraph" }; + paragraph2.Metadata["element_type"] = "heading"; + section.Elements.Add(paragraph2); + + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.Equal("text", chunks[0].Metadata["element_type"]); + } + + [Fact] + public async Task ProcessAsync_NullMetadataValuesSkipped() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var paragraph = new IngestionDocumentParagraph("Null meta text") { Text = "Null meta text" }; + paragraph.Metadata["element_type"] = null; + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.False(chunks[0].Metadata.ContainsKey("element_type")); + } + + [Fact] + public async Task ProcessAsync_ElementWithoutMetadata_Skipped() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var paragraph = new IngestionDocumentParagraph("Plain text") { Text = "Plain text" }; + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.Empty(chunks[0].Metadata); + } + + [Fact] + public async Task ProcessAsync_EmptyTextElement_Skipped() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + + var emptyParagraph = new IngestionDocumentParagraph(" ") { Text = "" }; + emptyParagraph.Metadata["element_type"] = "empty"; + section.Elements.Add(emptyParagraph); + + var visibleParagraph = new IngestionDocumentParagraph("Visible text") { Text = "Visible text" }; + section.Elements.Add(visibleParagraph); + + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.False(chunks[0].Metadata.ContainsKey("element_type")); + } + + [Fact] + public async Task ProcessAsync_SubstringMatchIsCaseSensitive() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var paragraph = new IngestionDocumentParagraph("Hello World") { Text = "Hello World" }; + paragraph.Metadata["element_type"] = "greeting"; + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Single(chunks); + Assert.Contains("Hello World", chunks[0].Content); + Assert.True(chunks[0].Metadata.ContainsKey("element_type")); + Assert.Equal("greeting", chunks[0].Metadata["element_type"]); + } + + [Fact] + public async Task ProcessAsync_EmptyDocument_ReturnsNoChunks() + { + var doc = new IngestionDocument("test.pdf"); + + var chunker = CreateChunker(); + var chunks = await CollectChunksAsync(chunker, doc); + + Assert.Empty(chunks); + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/PageImageRendererTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/PageImageRendererTests.cs new file mode 100644 index 0000000..c5b05bf --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/PageImageRendererTests.cs @@ -0,0 +1,92 @@ +#if NET8_0_OR_GREATER +using System; +using System.IO; +using UglyToad.PdfPig; +using UglyToad.PdfPig.Core; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class PageImageRendererTests +{ + [Fact] + public void RenderPage_NullPage_ThrowsArgumentNullException() + { + Assert.Throws(() => PageImageRenderer.RenderPage(null!)); + } + + [Fact] + public void RenderRegion_NullPage_ThrowsArgumentNullException() + { + var region = new PdfRectangle(0, 0, 100, 100); + Assert.Throws(() => PageImageRenderer.RenderRegion(null!, region)); + } + + [Fact] + public void RenderPage_ReturnsValidPng() + { + var path = IntegrationHelpers.GetDocumentPath("data"); + using var pdfDoc = PdfDocument.Open(path); + var page = pdfDoc.GetPage(1); + + var result = PageImageRenderer.RenderPage(page); + + Assert.NotNull(result); + Assert.True(result.Length > 4, "PNG output should have more than 4 bytes."); + // PNG magic bytes: 0x89 0x50 0x4E 0x47 + Assert.Equal(0x89, result[0]); + Assert.Equal(0x50, result[1]); + Assert.Equal(0x4E, result[2]); + Assert.Equal(0x47, result[3]); + } + + [Fact] + public void RenderPage_DifferentDpiProducesDifferentSizedOutput() + { + var path = IntegrationHelpers.GetDocumentPath("data"); + using var pdfDoc = PdfDocument.Open(path); + var page = pdfDoc.GetPage(1); + + var lowDpi = PageImageRenderer.RenderPage(page, dpi: 72); + var highDpi = PageImageRenderer.RenderPage(page, dpi: 300); + + // Higher DPI should produce a larger image + Assert.True(highDpi.Length > lowDpi.Length, + $"300 DPI image ({highDpi.Length} bytes) should be larger than 72 DPI image ({lowDpi.Length} bytes)."); + } + + [Fact] + public void RenderRegion_ReturnsValidPng() + { + var path = IntegrationHelpers.GetDocumentPath("data"); + using var pdfDoc = PdfDocument.Open(path); + var page = pdfDoc.GetPage(1); + + var region = new PdfRectangle(0, 0, page.Width, page.Height); + var result = PageImageRenderer.RenderRegion(page, region); + + Assert.NotNull(result); + Assert.True(result.Length > 4); + Assert.Equal(0x89, result[0]); + Assert.Equal(0x50, result[1]); + Assert.Equal(0x4E, result[2]); + Assert.Equal(0x47, result[3]); + } + + [Fact] + public void RenderRegion_SmallRegion_ProducesSmallerOutputThanFullPage() + { + var path = IntegrationHelpers.GetDocumentPath("data"); + using var pdfDoc = PdfDocument.Open(path); + var page = pdfDoc.GetPage(1); + + var fullPage = PageImageRenderer.RenderPage(page, dpi: 150); + var smallRegion = new PdfRectangle(0, 0, page.Width / 4, page.Height / 4); + var regionImage = PageImageRenderer.RenderRegion(page, smallRegion, dpi: 150); + + Assert.True(regionImage.Length < fullPage.Length, + $"Region image ({regionImage.Length} bytes) should be smaller than full page ({fullPage.Length} bytes)."); + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPig.DataIngestion.UnitTests.csproj b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPig.DataIngestion.UnitTests.csproj new file mode 100644 index 0000000..f31cb2f --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPig.DataIngestion.UnitTests.csproj @@ -0,0 +1,41 @@ + + + + CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests + CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests + net10.0 + true + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + PreserveNewest + + + + + + + + diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderDiTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderDiTests.cs new file mode 100644 index 0000000..7976286 --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderDiTests.cs @@ -0,0 +1,118 @@ +#if NET8_0_OR_GREATER +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using UglyToad.PdfPig.Content; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class PdfPigReaderDiTests +{ + [Fact] + public void AddPdfPigReader_RegistersPdfPigReader() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(); + + Assert.Contains(services, d => d.ServiceType == typeof(PdfPigReader)); + } + + [Fact] + public void AddPdfPigReader_ConfigureAppliesOptions() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(o => + { + o.Mode = PdfReadingMode.Hybrid; + o.RenderDpi = 300; + }); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var opts = options.Value; + + Assert.Equal(PdfReadingMode.Hybrid, opts.Mode); + Assert.Equal(300, opts.RenderDpi); + } + + [Fact] + public void AddPdfPigReader_DefaultOptionsWithoutConfigure() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var opts = options.Value; + + Assert.Equal(PdfReadingMode.TextOnly, opts.Mode); + Assert.Equal(150, opts.RenderDpi); + } + + [Fact] + public void AddPdfPigReader_InvalidDpi_ThrowsOnResolve() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(o => o.RenderDpi = 0); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var ex = Assert.Throws(() => options.Value); + + Assert.Contains("RenderDpi", ex.Message); + } + + [Fact] + public void IOptionsCtor_NullOptions_Throws() + { + Assert.Throws( + () => new PdfPigReader((IOptions)null!)); + } + + [Fact] + public async Task IOptionsCtor_UsesOptionsValues() + { + var options = Options.Create(new PdfPigReaderOptions + { + Mode = PdfReadingMode.Hybrid, + RenderDpi = 72 + }); + var reader = new PdfPigReader(options); + var pdfBytes = CreateBlankPagePdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "blank.pdf", "application/pdf"); + + Assert.Single(doc.Sections); + var section = doc.Sections[0]; + Assert.True(section.Metadata.ContainsKey("page_image"), + "Hybrid mode should render page images."); + } + + [Fact] + public void AddPdfPigReader_IdempotentRegistration() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(); + services.AddPdfPigReader(); + + var readerDescriptors = services + .Where(d => d.ServiceType == typeof(PdfPigReader)) + .ToList(); + + Assert.Single(readerDescriptors); + } + + private static byte[] CreateBlankPagePdf() + { + using var builder = new UglyToad.PdfPig.Writer.PdfDocumentBuilder(); + builder.AddPage(PageSize.A4); + return builder.Build(); + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderOptionsValidatorTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderOptionsValidatorTests.cs new file mode 100644 index 0000000..7162349 --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderOptionsValidatorTests.cs @@ -0,0 +1,82 @@ +#if NET8_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class PdfPigReaderOptionsValidatorTests +{ + [Fact] + public void DefaultOptions_Succeeds() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var opts = options.Value; + + Assert.Equal(PdfReadingMode.TextOnly, opts.Mode); + Assert.Equal(150, opts.RenderDpi); + } + + [Fact] + public void RenderDpi_Zero_Fails() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(o => o.RenderDpi = 0); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var ex = Assert.Throws(() => options.Value); + + Assert.Contains("RenderDpi", ex.Message); + } + + [Fact] + public void RenderDpi_Negative_Fails() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(o => o.RenderDpi = -1); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var ex = Assert.Throws(() => options.Value); + + Assert.Contains("RenderDpi", ex.Message); + } + + [Fact] + public void RenderDpi_One_Succeeds() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(o => o.RenderDpi = 1); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var opts = options.Value; + + Assert.Equal(1, opts.RenderDpi); + } + + [Fact] + public void CustomValidOptions_Succeeds() + { + var services = new ServiceCollection(); + services.AddPdfPigReader(o => + { + o.Mode = PdfReadingMode.Hybrid; + o.RenderDpi = 300; + }); + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + var opts = options.Value; + + Assert.Equal(PdfReadingMode.Hybrid, opts.Mode); + Assert.Equal(300, opts.RenderDpi); + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderTests.cs new file mode 100644 index 0000000..e95b1aa --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPigReaderTests.cs @@ -0,0 +1,499 @@ +#if NET8_0_OR_GREATER +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DataIngestion; +using UglyToad.PdfPig; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class PdfPigReaderTests +{ + #region Default (TextOnly) mode + + [Fact] + public async Task ReadAsync_DefaultMode_ReturnsDocumentWithSections() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + Assert.NotNull(doc); + Assert.Equal("data.pdf", doc.Identifier); + Assert.NotEmpty(doc.Sections); + } + + [Fact] + public async Task ReadAsync_DefaultMode_SectionCountMatchesPageCount() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("cat-genetics"); + + int expectedPages; + using (var pdfDoc = PdfDocument.Open(path)) + { + expectedPages = pdfDoc.NumberOfPages; + } + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "cat-genetics.pdf", "application/pdf"); + + Assert.Equal(expectedPages, doc.Sections.Count); + } + + [Fact] + public async Task ReadAsync_DefaultMode_SectionsContainCorrectPageNumbers() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + for (int i = 0; i < doc.Sections.Count; i++) + { + Assert.Equal(i + 1, doc.Sections[i].PageNumber); + } + } + + [Fact] + public async Task ReadAsync_DefaultMode_ParagraphsContainBoundingBoxMetadata() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + var elementsWithMetadata = doc.EnumerateContent() + .Where(e => e.HasMetadata) + .ToList(); + + Assert.NotEmpty(elementsWithMetadata); + + var first = elementsWithMetadata.First(); + Assert.True(first.Metadata.ContainsKey("BoundingBox.Left")); + Assert.True(first.Metadata.ContainsKey("BoundingBox.Bottom")); + Assert.True(first.Metadata.ContainsKey("BoundingBox.Right")); + Assert.True(first.Metadata.ContainsKey("BoundingBox.Top")); + } + + [Fact] + public async Task ReadAsync_DefaultMode_BoundingBoxValuesAreNumeric() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + var element = doc.EnumerateContent().First(e => e.HasMetadata); + + Assert.IsType(element.Metadata["BoundingBox.Left"]); + Assert.IsType(element.Metadata["BoundingBox.Bottom"]); + Assert.IsType(element.Metadata["BoundingBox.Right"]); + Assert.IsType(element.Metadata["BoundingBox.Top"]); + } + + [Fact] + public async Task ReadAsync_DefaultMode_ParagraphsHaveNonEmptyText() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + var allElements = doc.EnumerateContent().ToList(); + + foreach (var element in allElements) + { + Assert.False(string.IsNullOrEmpty(element.Text), + "All paragraphs should have non-empty text (empty blocks are skipped)."); + } + } + + [Fact] + public async Task ReadAsync_DefaultMode_ParagraphPageNumbersMatchSectionPageNumbers() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + foreach (var section in doc.Sections) + { + foreach (var element in section.Elements) + { + Assert.Equal(section.PageNumber, element.PageNumber); + } + } + } + + [Fact] + public async Task ReadAsync_WithExplicitSegmenter_ReturnsStructuredSections() + { + var reader = new PdfPigReader(RecursiveXYCut.Instance); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + Assert.NotNull(doc); + Assert.NotEmpty(doc.Sections); + } + + [Fact] + public async Task ReadAsync_MinimalPdf_ReturnsDocument() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("inherited_mediabox"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "inherited_mediabox.pdf", "application/pdf"); + + Assert.NotNull(doc); + Assert.Equal("inherited_mediabox.pdf", doc.Identifier); + } + + [Fact] + public async Task ReadAsync_CancellationAlreadyCancelled_ThrowsOperationCanceledException() + { + var reader = new PdfPigReader(); + var path = IntegrationHelpers.GetDocumentPath("data"); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + using var stream = File.OpenRead(path); + + await Assert.ThrowsAnyAsync( + () => reader.ReadAsync(stream, "data.pdf", "application/pdf", cts.Token)); + } + + #endregion + + #region TextOnly mode + + [Fact] + public async Task ReadAsync_TextOnly_DoesNotStorePageImage() + { + var reader = new PdfPigReader(mode: PdfReadingMode.TextOnly); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + foreach (var section in doc.Sections) + { + Assert.False(section.Metadata.ContainsKey("page_image"), + $"Section for page {section.PageNumber} should NOT contain page_image in TextOnly mode."); + Assert.False(section.Metadata.ContainsKey("page_width")); + Assert.False(section.Metadata.ContainsKey("page_height")); + } + } + + [Fact] + public async Task ReadAsync_TextOnly_BlankPage_NoPlaceholder() + { + var reader = new PdfPigReader(mode: PdfReadingMode.TextOnly); + var pdfBytes = CreateBlankPagePdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "blank.pdf", "application/pdf"); + + Assert.Single(doc.Sections); + Assert.Empty(doc.Sections[0].Elements); + } + + #endregion + + #region Hybrid mode + + [Fact] + public async Task ReadAsync_Hybrid_StoresPageImageInSectionMetadata() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + foreach (var section in doc.Sections) + { + Assert.True(section.Metadata.ContainsKey("page_image"), + $"Section for page {section.PageNumber} should contain page_image in Hybrid mode."); + } + } + + [Fact] + public async Task ReadAsync_Hybrid_PageImageIsValidPng() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + var section = doc.Sections[0]; + var imageBytes = section.Metadata["page_image"] as byte[]; + + Assert.NotNull(imageBytes); + Assert.True(imageBytes.Length > 4); + // PNG magic bytes + Assert.Equal(0x89, imageBytes[0]); + Assert.Equal(0x50, imageBytes[1]); + Assert.Equal(0x4E, imageBytes[2]); + Assert.Equal(0x47, imageBytes[3]); + } + + [Fact] + public async Task ReadAsync_Hybrid_StoresPageDimensions() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + var section = doc.Sections[0]; + Assert.True(section.Metadata.ContainsKey("page_width")); + Assert.True(section.Metadata.ContainsKey("page_height")); + Assert.IsType(section.Metadata["page_width"]); + Assert.IsType(section.Metadata["page_height"]); + } + + [Fact] + public async Task ReadAsync_Hybrid_CustomDpi_ProducesValidDocument() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid, renderDpi: 72); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + Assert.NotNull(doc); + Assert.NotEmpty(doc.Sections); + + var imageBytes = doc.Sections[0].Metadata["page_image"] as byte[]; + Assert.NotNull(imageBytes); + Assert.Equal(0x89, imageBytes[0]); + Assert.Equal(0x50, imageBytes[1]); + } + + [Fact] + public async Task ReadAsync_Hybrid_WithAllParameters_ProducesValidDocument() + { + var reader = new PdfPigReader( + segmenter: RecursiveXYCut.Instance, + mode: PdfReadingMode.Hybrid, + renderDpi: 200); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + Assert.NotNull(doc); + Assert.NotEmpty(doc.Sections); + Assert.True(doc.Sections[0].Metadata.ContainsKey("page_image")); + } + + [Fact] + public async Task ReadAsync_Hybrid_BlankPage_CreatesPlaceholderElement() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var pdfBytes = CreateBlankPagePdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "blank.pdf", "application/pdf"); + + Assert.Single(doc.Sections); + var section = doc.Sections[0]; + Assert.Single(section.Elements); + + var element = section.Elements[0]; + Assert.Equal(string.Empty, element.Text); + } + + [Fact] + public async Task ReadAsync_Hybrid_BlankPage_PlaceholderHasCorrectPageNumber() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var pdfBytes = CreateBlankPagePdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "blank.pdf", "application/pdf"); + + var element = doc.Sections[0].Elements[0]; + Assert.Equal(1, element.PageNumber); + } + + [Fact] + public async Task ReadAsync_Hybrid_BlankPage_PlaceholderHasPlaceholderMetadata() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var pdfBytes = CreateBlankPagePdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "blank.pdf", "application/pdf"); + + var element = doc.Sections[0].Elements[0]; + Assert.True(element.HasMetadata); + Assert.True(element.Metadata.ContainsKey("placeholder")); + Assert.Equal(true, element.Metadata["placeholder"]); + } + + [Fact] + public async Task ReadAsync_Hybrid_BlankPage_SectionStillHasPageImage() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var pdfBytes = CreateBlankPagePdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "blank.pdf", "application/pdf"); + + var section = doc.Sections[0]; + Assert.True(section.Metadata.ContainsKey("page_image")); + var imageBytes = section.Metadata["page_image"] as byte[]; + Assert.NotNull(imageBytes); + Assert.True(imageBytes.Length > 0); + } + + [Fact] + public async Task ReadAsync_Hybrid_MixedPdf_OnlyBlankPagesGetPlaceholders() + { + var reader = new PdfPigReader(mode: PdfReadingMode.Hybrid); + var pdfBytes = CreateMixedPdf(); + + using var stream = new MemoryStream(pdfBytes); + var doc = await reader.ReadAsync(stream, "mixed.pdf", "application/pdf"); + + Assert.Equal(2, doc.Sections.Count); + + // Page 1 has text — should have normal elements, no placeholder + var page1 = doc.Sections[0]; + Assert.NotEmpty(page1.Elements); + foreach (var el in page1.Elements) + { + Assert.False(string.IsNullOrEmpty(el.Text)); + Assert.False(el.Metadata.ContainsKey("placeholder")); + } + + // Page 2 is blank — should have single placeholder + var page2 = doc.Sections[1]; + Assert.Single(page2.Elements); + Assert.Equal(string.Empty, page2.Elements[0].Text); + Assert.True(page2.Elements[0].Metadata.ContainsKey("placeholder")); + Assert.Equal(2, page2.Elements[0].PageNumber); + } + + #endregion + + #region VisionOnly mode + + [Fact] + public async Task ReadAsync_VisionOnly_EveryPageGetsPlaceholder() + { + var reader = new PdfPigReader(mode: PdfReadingMode.VisionOnly); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + Assert.NotEmpty(doc.Sections); + + foreach (var section in doc.Sections) + { + Assert.Single(section.Elements); + var element = section.Elements[0]; + Assert.Equal(string.Empty, element.Text); + Assert.True(element.Metadata.ContainsKey("placeholder")); + Assert.Equal(true, element.Metadata["placeholder"]); + } + } + + [Fact] + public async Task ReadAsync_VisionOnly_AllSectionsHavePageImage() + { + var reader = new PdfPigReader(mode: PdfReadingMode.VisionOnly); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + foreach (var section in doc.Sections) + { + Assert.True(section.Metadata.ContainsKey("page_image")); + var imageBytes = section.Metadata["page_image"] as byte[]; + Assert.NotNull(imageBytes); + Assert.True(imageBytes.Length > 0); + } + } + + [Fact] + public async Task ReadAsync_VisionOnly_PlaceholderPageNumbersMatchSections() + { + var reader = new PdfPigReader(mode: PdfReadingMode.VisionOnly); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + for (int i = 0; i < doc.Sections.Count; i++) + { + Assert.Equal(i + 1, doc.Sections[i].Elements[0].PageNumber); + } + } + + [Fact] + public async Task ReadAsync_VisionOnly_SkipsTextExtraction() + { + // VisionOnly should produce placeholders even for PDFs with text + var reader = new PdfPigReader(mode: PdfReadingMode.VisionOnly); + var path = IntegrationHelpers.GetDocumentPath("data"); + + using var stream = File.OpenRead(path); + var doc = await reader.ReadAsync(stream, "data.pdf", "application/pdf"); + + // All elements should be placeholders with empty text — no native text extraction + foreach (var element in doc.EnumerateContent()) + { + Assert.Equal(string.Empty, element.Text); + Assert.True(element.Metadata.ContainsKey("placeholder")); + } + } + + #endregion + + #region Helpers + + private static byte[] CreateBlankPagePdf() + { + using var builder = new UglyToad.PdfPig.Writer.PdfDocumentBuilder(); + builder.AddPage(PageSize.A4); + return builder.Build(); + } + + private static byte[] CreateMixedPdf() + { + using var builder = new UglyToad.PdfPig.Writer.PdfDocumentBuilder(); + // Page 1: has text + var page1 = builder.AddPage(PageSize.A4); + var font = builder.AddStandard14Font(UglyToad.PdfPig.Fonts.Standard14Fonts.Standard14Font.Helvetica); + page1.AddText("Hello World", 12, new UglyToad.PdfPig.Core.PdfPoint(72, 720), font); + // Page 2: blank (no text) + builder.AddPage(PageSize.A4); + return builder.Build(); + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/VisionOcrEnricherTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/VisionOcrEnricherTests.cs new file mode 100644 index 0000000..5fcf72c --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/VisionOcrEnricherTests.cs @@ -0,0 +1,292 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.Processors; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class VisionOcrEnricherTests +{ + private class TestChatClient : IChatClient + { + private readonly string _response; + + public List LastMessages { get; private set; } = new(); + + public TestChatClient(string response) => _response = response; + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + LastMessages = messages.ToList(); + var msg = new ChatMessage(ChatRole.Assistant, _response); + return Task.FromResult(new ChatResponse(msg)); + } + + public IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() { } + } + + [Fact] + public void Constructor_NullChatClient_ThrowsArgumentNullException() + { + Assert.Throws(() => new VisionOcrEnricher(null!)); + } + + [Fact] + public async Task ProcessAsync_EmptyTextElement_GetsOcrText() + { + var ocrText = "Extracted OCR text from image"; + var client = new TestChatClient(ocrText); + var fallback = new VisionOcrEnricher(client); + + var doc = CreateDocumentWithEmptyTextElement(); + + var result = await fallback.ProcessAsync(doc); + + var element = result.EnumerateContent().First(); + Assert.Equal(ocrText, element.Text); + } + + [Fact] + public async Task ProcessAsync_EmptyTextElement_SetsOcrSourceMetadata() + { + var client = new TestChatClient("OCR result"); + var fallback = new VisionOcrEnricher(client); + + var doc = CreateDocumentWithEmptyTextElement(); + + var result = await fallback.ProcessAsync(doc); + + var element = result.EnumerateContent().First(); + Assert.True(element.Metadata.ContainsKey("ocr_source")); + Assert.Equal("vision_llm", element.Metadata["ocr_source"]); + } + + [Fact] + public async Task ProcessAsync_ElementWithText_NotModified() + { + var client = new TestChatClient("Should not replace"); + var fallback = new VisionOcrEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var paragraph = new IngestionDocumentParagraph("Existing content") { Text = "Existing content" }; + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var result = await fallback.ProcessAsync(doc); + + var element = result.EnumerateContent().First(); + Assert.Equal("Existing content", element.Text); + Assert.False(element.Metadata.ContainsKey("ocr_source")); + } + + [Fact] + public async Task ProcessAsync_MixedElements_OnlyEmptyOnesEnriched() + { + var ocrText = "Extracted text"; + var client = new TestChatClient(ocrText); + var fallback = new VisionOcrEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + + var withText = new IngestionDocumentParagraph("Has content") { Text = "Has content" }; + var withoutText = new IngestionDocumentParagraph("empty region") { Text = "" }; + + section.Elements.Add(withText); + section.Elements.Add(withoutText); + doc.Sections.Add(section); + + var result = await fallback.ProcessAsync(doc); + var elements = result.EnumerateContent().ToList(); + + Assert.Equal("Has content", elements[0].Text); + Assert.False(elements[0].Metadata.ContainsKey("ocr_source")); + + Assert.Equal(ocrText, elements[1].Text); + Assert.True(elements[1].Metadata.ContainsKey("ocr_source")); + } + + [Fact] + public async Task ProcessAsync_EmptyDocument_NoException() + { + var client = new TestChatClient("response"); + var fallback = new VisionOcrEnricher(client); + + var doc = new IngestionDocument("empty.pdf"); + + var result = await fallback.ProcessAsync(doc); + + Assert.NotNull(result); + } + + [Fact] + public async Task ProcessAsync_LlmReturnsWhitespace_ElementNotModified() + { + var client = new TestChatClient(" "); + var fallback = new VisionOcrEnricher(client); + + var doc = CreateDocumentWithEmptyTextElement(); + + var result = await fallback.ProcessAsync(doc); + + var element = result.EnumerateContent().First(); + Assert.True(string.IsNullOrWhiteSpace(element.Text)); + Assert.False(element.Metadata.ContainsKey("ocr_source")); + } + + [Fact] + public async Task ProcessAsync_CancellationRequested_Throws() + { + var client = new TestChatClient("response"); + var fallback = new VisionOcrEnricher(client); + + var doc = CreateDocumentWithEmptyTextElement(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + () => fallback.ProcessAsync(doc, cts.Token)); + } + + [Fact] + public async Task ProcessAsync_ReturnsSameDocumentInstance() + { + var client = new TestChatClient("ocr text"); + var fallback = new VisionOcrEnricher(client); + + var doc = CreateDocumentWithEmptyTextElement(); + + var result = await fallback.ProcessAsync(doc); + + Assert.Same(doc, result); + } + + [Fact] + public async Task ProcessAsync_NullTextElement_GetsOcrText() + { + var ocrText = "OCR from null text"; + var client = new TestChatClient(ocrText); + var fallback = new VisionOcrEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + // Text defaults to null when not explicitly set + var paragraph = new IngestionDocumentParagraph("image placeholder"); + section.Elements.Add(paragraph); + doc.Sections.Add(section); + + var result = await fallback.ProcessAsync(doc); + + var element = result.EnumerateContent().First(); + // If Text was null (IsNullOrWhiteSpace), it should get OCR'd + if (string.IsNullOrWhiteSpace(element.Text) == false) + { + // Text was set by OCR + Assert.Equal(ocrText, element.Text); + } + } + + [Fact] + public async Task ProcessAsync_WithPageImage_SendsDataContentToLlm() + { + var ocrText = "Vision OCR result"; + var client = new TestChatClient(ocrText); + var fallback = new VisionOcrEnricher(client); + + var fakeImageBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + section.Metadata["page_image"] = fakeImageBytes; + var emptyParagraph = new IngestionDocumentParagraph("image region") { Text = "", PageNumber = 1 }; + section.Elements.Add(emptyParagraph); + doc.Sections.Add(section); + + await fallback.ProcessAsync(doc); + + var userMsg = client.LastMessages.Last(m => m.Role == ChatRole.User); + Assert.Contains(userMsg.Contents, c => c is DataContent dc && dc.MediaType == "image/png"); + } + + [Fact] + public async Task ProcessAsync_WithoutPageImage_SendsTextOnlyToLlm() + { + var ocrText = "Text fallback OCR result"; + var client = new TestChatClient(ocrText); + var fallback = new VisionOcrEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + // No page_image metadata + var emptyParagraph = new IngestionDocumentParagraph("image region") { Text = "", PageNumber = 1 }; + section.Elements.Add(emptyParagraph); + doc.Sections.Add(section); + + await fallback.ProcessAsync(doc); + + var userMsg = client.LastMessages.Last(m => m.Role == ChatRole.User); + Assert.DoesNotContain(userMsg.Contents, c => c is DataContent); + } + + [Fact] + public async Task ProcessAsync_PlaceholderElement_WithPageImage_PerformsVisionOcr() + { + var ocrText = "Text extracted from scanned page image"; + var client = new TestChatClient(ocrText); + var enricher = new VisionOcrEnricher(client); + + // Simulate what PdfPigReader produces for a scanned page + var fakeImageBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + var doc = new IngestionDocument("scanned.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + section.Metadata["page_image"] = fakeImageBytes; + + var placeholder = new IngestionDocumentParagraph("[scanned-page]") + { + Text = string.Empty, + PageNumber = 1 + }; + placeholder.Metadata["placeholder"] = true; + section.Elements.Add(placeholder); + doc.Sections.Add(section); + + var result = await enricher.ProcessAsync(doc); + + var element = result.EnumerateContent().First(); + Assert.Equal(ocrText, element.Text); + Assert.Equal("vision_llm", element.Metadata["ocr_source"]); + + // Verify vision approach was used (DataContent with image) + var userMsg = client.LastMessages.Last(m => m.Role == ChatRole.User); + Assert.Contains(userMsg.Contents, c => c is DataContent dc && dc.MediaType == "image/png"); + } + + private static IngestionDocument CreateDocumentWithEmptyTextElement() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + var emptyParagraph = new IngestionDocumentParagraph("image region") { Text = "" }; + section.Elements.Add(emptyParagraph); + doc.Sections.Add(section); + return doc; + } +} +#endif diff --git a/MEDI/test/PdfPig.DataIngestion.UnitTests/VisionTableEnricherTests.cs b/MEDI/test/PdfPig.DataIngestion.UnitTests/VisionTableEnricherTests.cs new file mode 100644 index 0000000..8d3ec70 --- /dev/null +++ b/MEDI/test/PdfPig.DataIngestion.UnitTests/VisionTableEnricherTests.cs @@ -0,0 +1,242 @@ +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.Processors; +using Xunit; + +namespace CommunityToolkit.DocumentProcessing.PdfPig.DataIngestion.UnitTests; + +public class VisionTableEnricherTests +{ + private class TestChatClient : IChatClient + { + private readonly string _response; + + public List LastMessages { get; private set; } = new(); + + public TestChatClient(string response) => _response = response; + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + LastMessages = messages.ToList(); + var msg = new ChatMessage(ChatRole.Assistant, _response); + return Task.FromResult(new ChatResponse(msg)); + } + + public IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() { } + } + + [Fact] + public void Constructor_NullChatClient_ThrowsArgumentNullException() + { + Assert.Throws(() => new VisionTableEnricher(null!)); + } + + [Fact] + public async Task ProcessAsync_TableElement_GetsEnrichedWithMarkdown() + { + var expectedMarkdown = "| H1 | H2 |\n|---|---|\n| V1 | V2 |"; + var client = new TestChatClient(expectedMarkdown); + var enricher = new VisionTableEnricher(client); + + var doc = CreateDocumentWithTable(); + + var result = await enricher.ProcessAsync(doc); + + var table = result.EnumerateContent().OfType().First(); + Assert.True(table.HasMetadata); + Assert.True(table.Metadata.ContainsKey("enriched_markdown_table")); + Assert.Equal(expectedMarkdown, table.Metadata["enriched_markdown_table"]); + } + + [Fact] + public async Task ProcessAsync_NonTableElement_NotModified() + { + var client = new TestChatClient("should not appear"); + var enricher = new VisionTableEnricher(client); + + var doc = CreateDocumentWithTable(); + + var result = await enricher.ProcessAsync(doc); + + var paragraphs = result.EnumerateContent() + .OfType() + .ToList(); + + foreach (var p in paragraphs) + { + Assert.False(p.Metadata.ContainsKey("enriched_markdown_table")); + } + } + + [Fact] + public async Task ProcessAsync_EmptyDocument_NoException() + { + var client = new TestChatClient("response"); + var enricher = new VisionTableEnricher(client); + + var doc = new IngestionDocument("empty.pdf"); + + var result = await enricher.ProcessAsync(doc); + + Assert.NotNull(result); + Assert.Empty(result.Sections); + } + + [Fact] + public async Task ProcessAsync_DocumentWithNoTables_NoEnrichment() + { + var client = new TestChatClient("response"); + var enricher = new VisionTableEnricher(client); + + var doc = new IngestionDocument("no-tables.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + section.Elements.Add(new IngestionDocumentParagraph("Regular text") { Text = "Regular text" }); + doc.Sections.Add(section); + + var result = await enricher.ProcessAsync(doc); + + var elements = result.EnumerateContent().ToList(); + foreach (var e in elements) + { + Assert.False(e.Metadata.ContainsKey("enriched_markdown_table")); + } + } + + [Fact] + public async Task ProcessAsync_MultipleTables_AllEnriched() + { + var expectedMarkdown = "| A | B |"; + var client = new TestChatClient(expectedMarkdown); + var enricher = new VisionTableEnricher(client); + + var doc = new IngestionDocument("multi-table.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + + section.Elements.Add(CreateTable("| T1C1 | T1C2 |")); + section.Elements.Add(CreateTable("| T2C1 | T2C2 |")); + doc.Sections.Add(section); + + var result = await enricher.ProcessAsync(doc); + + var tables = result.EnumerateContent().OfType().ToList(); + Assert.Equal(2, tables.Count); + Assert.All(tables, t => + { + Assert.True(t.Metadata.ContainsKey("enriched_markdown_table")); + Assert.Equal(expectedMarkdown, t.Metadata["enriched_markdown_table"]); + }); + } + + [Fact] + public async Task ProcessAsync_CancellationRequested_Throws() + { + var client = new TestChatClient("response"); + var enricher = new VisionTableEnricher(client); + + var doc = CreateDocumentWithTable(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + () => enricher.ProcessAsync(doc, cts.Token)); + } + + [Fact] + public async Task ProcessAsync_ReturnsSameDocumentInstance() + { + var client = new TestChatClient("markdown"); + var enricher = new VisionTableEnricher(client); + + var doc = CreateDocumentWithTable(); + + var result = await enricher.ProcessAsync(doc); + + Assert.Same(doc, result); + } + + [Fact] + public async Task ProcessAsync_WithPageImage_SendsDataContentToLlm() + { + var expectedMarkdown = "| A | B |"; + var client = new TestChatClient(expectedMarkdown); + var enricher = new VisionTableEnricher(client); + + var fakeImageBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + section.Metadata["page_image"] = fakeImageBytes; + + var table = CreateTable("| raw | data |"); + table.PageNumber = 1; + section.Elements.Add(table); + doc.Sections.Add(section); + + await enricher.ProcessAsync(doc); + + var userMsg = client.LastMessages.Last(m => m.Role == ChatRole.User); + Assert.Contains(userMsg.Contents, c => c is DataContent dc && dc.MediaType == "image/png"); + } + + [Fact] + public async Task ProcessAsync_WithoutPageImage_SendsTextOnlyToLlm() + { + var expectedMarkdown = "| A | B |"; + var client = new TestChatClient(expectedMarkdown); + var enricher = new VisionTableEnricher(client); + + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + // No page_image metadata + + var table = CreateTable("| raw | data |"); + table.PageNumber = 1; + section.Elements.Add(table); + doc.Sections.Add(section); + + await enricher.ProcessAsync(doc); + + var userMsg = client.LastMessages.Last(m => m.Role == ChatRole.User); + Assert.DoesNotContain(userMsg.Contents, c => c is DataContent); + } + + private static IngestionDocument CreateDocumentWithTable() + { + var doc = new IngestionDocument("test.pdf"); + var section = new IngestionDocumentSection { PageNumber = 1 }; + + section.Elements.Add(CreateTable("| H1 | H2 |\n|---|---|\n| V1 | V2 |")); + + var paragraph = new IngestionDocumentParagraph("Regular text") { Text = "Regular text" }; + section.Elements.Add(paragraph); + + doc.Sections.Add(section); + return doc; + } + + private static IngestionDocumentTable CreateTable(string markdown) + { + var cells = new IngestionDocumentElement[1, 2]; + cells[0, 0] = new IngestionDocumentParagraph("C1") { Text = "C1" }; + cells[0, 1] = new IngestionDocumentParagraph("C2") { Text = "C2" }; + return new IngestionDocumentTable(markdown, cells); + } +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/AnnotatedTextBlockTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/AnnotatedTextBlockTests.cs new file mode 100644 index 0000000..efd9b23 --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/AnnotatedTextBlockTests.cs @@ -0,0 +1,61 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.Core; +using UglyToad.PdfPig.DocumentLayoutAnalysis; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using UglyToad.PdfPig.PdfFonts; +using Xunit; + +public class AnnotatedTextBlockTests +{ + [Fact] + public void Constructor_SetsLabelAndConfidence() + { + var word = CreateWord(new PdfRectangle(10, 10, 50, 25)); + var line = new TextLine(new[] { word }); + var block = new AnnotatedTextBlock(new[] { line }, "table", 0.95f); + + Assert.Equal("table", block.Label); + Assert.Equal(0.95f, block.Confidence, 4); + } + + [Fact] + public void TextProperty_ReturnsLineText() + { + var word = CreateWord(new PdfRectangle(10, 10, 50, 25)); + var line = new TextLine(new[] { word }); + var block = new AnnotatedTextBlock(new[] { line }, "text", 0.9f); + + Assert.Equal("a", block.Text); + } + + [Fact] + public void DefaultSeparator_IsNewline() + { + var w1 = CreateWord(new PdfRectangle(10, 100, 50, 120)); + var w2 = CreateWord(new PdfRectangle(10, 50, 50, 70)); + var line1 = new TextLine(new[] { w1 }); + var line2 = new TextLine(new[] { w2 }); + var block = new AnnotatedTextBlock(new[] { line1, line2 }, "text", 0.8f); + + Assert.Equal("a\na", block.Text); + } + + private static Word CreateWord(PdfRectangle boundingBox) + { + var letter = new Letter( + "a", + boundingBox, + boundingBox, + boundingBox.BottomLeft, + boundingBox.BottomRight, + 10, 1, + (FontDetails)null!, + TextRenderingMode.NeitherClip, + null!, null!, + 0, 0); + return new Word(new[] { letter }); + } +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ConfigurableLayoutModelTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ConfigurableLayoutModelTests.cs new file mode 100644 index 0000000..1c9448d --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ConfigurableLayoutModelTests.cs @@ -0,0 +1,82 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using System; +using System.Collections.Generic; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; +using Xunit; + +public class ConfigurableLayoutModelTests +{ + #region Constructor guard clauses + + [Fact] + public void Constructor_NullModelPath_ThrowsArgumentNullException() + { + var ex = Assert.Throws( + () => new ConfigurableLayoutModel(null!, new LayoutModelOptions())); + + Assert.Equal("modelPath", ex.ParamName); + } + + [Fact] + public void Constructor_NullOptions_ThrowsArgumentNullException() + { + var ex = Assert.Throws( + () => new ConfigurableLayoutModel("model.onnx", null!)); + + Assert.Equal("options", ex.ParamName); + } + + #endregion + + #region Properties + + [Fact] + public void ModelPath_ReturnsConstructorValue() + { + var model = new ConfigurableLayoutModel("path/to/model.onnx", new LayoutModelOptions()); + + Assert.Equal("path/to/model.onnx", model.ModelPath); + } + + [Fact] + public void LabelMapping_WithClassLabels_ReturnsSameLabels() + { + var labels = new Dictionary { [0] = "text", [1] = "table" }; + var options = new LayoutModelOptions { ClassLabels = labels }; + var model = new ConfigurableLayoutModel("model.onnx", options); + + var mapping = model.LabelMapping; + + Assert.Equal(2, mapping.Count); + Assert.Equal("text", mapping[0]); + Assert.Equal("table", mapping[1]); + } + + [Fact] + public void LabelMapping_WithoutClassLabels_ReturnsEmptyDictionary() + { + var model = new ConfigurableLayoutModel("model.onnx", new LayoutModelOptions()); + + var mapping = model.LabelMapping; + + Assert.NotNull(mapping); + Assert.Empty(mapping); + } + + #endregion + + #region Dispose + + [Fact] + public void Dispose_CalledTwice_DoesNotThrow() + { + var model = new ConfigurableLayoutModel("model.onnx", new LayoutModelOptions()); + + model.Dispose(); + model.Dispose(); + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/DetectionPostprocessingTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/DetectionPostprocessingTests.cs new file mode 100644 index 0000000..a3e4d59 --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/DetectionPostprocessingTests.cs @@ -0,0 +1,314 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using UglyToad.PdfPig.Core; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using Xunit; + +public class DetectionPostprocessingTests +{ + #region CxCyWhToRect + + [Fact] + public void CxCyWhToRect_CenteredBox() + { + // Center at (320, 240) in image coords, 100×50 box on a 640×480 page + var rect = DetectionPostprocessing.CxCyWhToRect(320, 240, 100, 50, 640, 480); + + // Image coords: x1=270, y1=215, x2=370, y2=265 + // PDF Y-flip: pdfBottom = 480 - 265 = 215, pdfTop = 480 - 215 = 265 + Assert.Equal(270, rect.Left, 1); + Assert.Equal(370, rect.Right, 1); + Assert.Equal(215, rect.Bottom, 1); + Assert.Equal(265, rect.Top, 1); + } + + [Fact] + public void CxCyWhToRect_TopLeftCornerBox() + { + // Box centered at (25, 25) with size 50×50 on 100×100 page + var rect = DetectionPostprocessing.CxCyWhToRect(25, 25, 50, 50, 100, 100); + + // Image coords: x1=0, y1=0, x2=50, y2=50 + // PDF Y-flip: pdfBottom = 100-50=50, pdfTop = 100-0=100 + Assert.Equal(0, rect.Left, 1); + Assert.Equal(50, rect.Right, 1); + Assert.Equal(50, rect.Bottom, 1); + Assert.Equal(100, rect.Top, 1); + } + + [Fact] + public void CxCyWhToRect_BottomRightCorner() + { + // Box centered at (75, 75) with size 50×50 on 100×100 page + var rect = DetectionPostprocessing.CxCyWhToRect(75, 75, 50, 50, 100, 100); + + // Image coords: x1=50, y1=50, x2=100, y2=100 + // PDF Y-flip: pdfBottom = 100-100=0, pdfTop = 100-50=50 + Assert.Equal(50, rect.Left, 1); + Assert.Equal(100, rect.Right, 1); + Assert.Equal(0, rect.Bottom, 1); + Assert.Equal(50, rect.Top, 1); + } + + #endregion + + #region XyxyToRect + + [Fact] + public void XyxyToRect_FlipsYAxis() + { + // Image coords: top-left (10, 20), bottom-right (100, 80) on 200×300 page + var rect = DetectionPostprocessing.XyxyToRect(10, 20, 100, 80, 200, 300); + + Assert.Equal(10, rect.Left, 1); + Assert.Equal(100, rect.Right, 1); + // PDF Y-flip: pdfBottom = 300 - 80 = 220, pdfTop = 300 - 20 = 280 + Assert.Equal(220, rect.Bottom, 1); + Assert.Equal(280, rect.Top, 1); + } + + [Fact] + public void XyxyToRect_FullPage() + { + // Image covers full page 640×480 + var rect = DetectionPostprocessing.XyxyToRect(0, 0, 640, 480, 640, 480); + + Assert.Equal(0, rect.Left, 1); + Assert.Equal(640, rect.Right, 1); + Assert.Equal(0, rect.Bottom, 1); + Assert.Equal(480, rect.Top, 1); + } + + [Fact] + public void XyxyToRect_SmallBox() + { + var rect = DetectionPostprocessing.XyxyToRect(0, 0, 10, 10, 1000, 1000); + + Assert.Equal(0, rect.Left, 1); + Assert.Equal(10, rect.Right, 1); + Assert.Equal(990, rect.Bottom, 1); + Assert.Equal(1000, rect.Top, 1); + } + + #endregion + + #region ComputeIoU + + [Fact] + public void ComputeIoU_IdenticalBoxes_ReturnsOne() + { + var a = new PdfRectangle(0, 0, 100, 100); + var b = new PdfRectangle(0, 0, 100, 100); + + float iou = DetectionPostprocessing.ComputeIoU(a, b); + + Assert.Equal(1f, iou, 4); + } + + [Fact] + public void ComputeIoU_NonOverlapping_ReturnsZero() + { + var a = new PdfRectangle(0, 0, 50, 50); + var b = new PdfRectangle(100, 100, 200, 200); + + float iou = DetectionPostprocessing.ComputeIoU(a, b); + + Assert.Equal(0f, iou, 4); + } + + [Fact] + public void ComputeIoU_EdgeAdjacent_ReturnsZero() + { + // Two boxes sharing an edge (no overlap area) + var a = new PdfRectangle(0, 0, 50, 50); + var b = new PdfRectangle(50, 0, 100, 50); + + float iou = DetectionPostprocessing.ComputeIoU(a, b); + + Assert.Equal(0f, iou, 4); + } + + [Fact] + public void ComputeIoU_HalfOverlap() + { + // Two 100×100 boxes overlapping by 50 in X + var a = new PdfRectangle(0, 0, 100, 100); + var b = new PdfRectangle(50, 0, 150, 100); + + float iou = DetectionPostprocessing.ComputeIoU(a, b); + + // Intersection: 50×100 = 5000 + // Union: 10000 + 10000 - 5000 = 15000 + // IoU = 5000/15000 = 1/3 + Assert.Equal(1f / 3f, iou, 3); + } + + [Fact] + public void ComputeIoU_ContainedBox() + { + // Small box fully inside large box + var large = new PdfRectangle(0, 0, 100, 100); + var small = new PdfRectangle(25, 25, 75, 75); + + float iou = DetectionPostprocessing.ComputeIoU(large, small); + + // Intersection = 50×50 = 2500 + // Union = 10000 + 2500 - 2500 = 10000 + // IoU = 2500/10000 = 0.25 + Assert.Equal(0.25f, iou, 3); + } + + [Fact] + public void ComputeIoU_SymmetricProperty() + { + var a = new PdfRectangle(10, 10, 60, 80); + var b = new PdfRectangle(30, 20, 90, 70); + + Assert.Equal( + DetectionPostprocessing.ComputeIoU(a, b), + DetectionPostprocessing.ComputeIoU(b, a), + 4); + } + + #endregion + + #region ApplyNms + + [Fact] + public void ApplyNms_EmptyList_ReturnsEmpty() + { + var detections = new List(); + var result = DetectionPostprocessing.ApplyNms(detections); + + Assert.Empty(result); + } + + [Fact] + public void ApplyNms_SingleDetection_ReturnsSame() + { + var detection = new LayoutDetection(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.9f); + var detections = new List { detection }; + + var result = DetectionPostprocessing.ApplyNms(detections); + + Assert.Single(result); + Assert.Equal(detection, result[0]); + } + + [Fact] + public void ApplyNms_NonOverlapping_AllPreserved() + { + var detections = new List + { + new(new PdfRectangle(0, 0, 50, 50), "text", 0, 0.9f), + new(new PdfRectangle(200, 200, 300, 300), "text", 0, 0.8f), + new(new PdfRectangle(400, 400, 500, 500), "text", 0, 0.7f) + }; + + var result = DetectionPostprocessing.ApplyNms(detections); + + Assert.Equal(3, result.Count); + } + + [Fact] + public void ApplyNms_IdenticalBoxes_KeepsHighestConfidence() + { + var detections = new List + { + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.5f), + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.9f), + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.7f) + }; + + var result = DetectionPostprocessing.ApplyNms(detections, 0.45f); + + Assert.Single(result); + Assert.Equal(0.9f, result[0].Confidence, 4); + } + + [Fact] + public void ApplyNms_HighOverlap_Suppressed() + { + // Two boxes with ~70% overlap + var detections = new List + { + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.9f), + new(new PdfRectangle(20, 20, 120, 120), "text", 0, 0.7f) + }; + + var result = DetectionPostprocessing.ApplyNms(detections, 0.3f); + + // IoU of these boxes is significant → lower-confidence one suppressed + Assert.Single(result); + Assert.Equal(0.9f, result[0].Confidence, 4); + } + + [Fact] + public void ApplyNms_HighThreshold_KeepsBoth() + { + // Same boxes, but with very high IoU threshold → both kept + var detections = new List + { + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.9f), + new(new PdfRectangle(20, 20, 120, 120), "text", 0, 0.7f) + }; + + var result = DetectionPostprocessing.ApplyNms(detections, 0.99f); + + Assert.Equal(2, result.Count); + } + + [Fact] + public void ApplyNms_NullDetections_Throws() + { + Assert.Throws(() => + DetectionPostprocessing.ApplyNms(null!)); + } + + [Fact] + public void ApplyNms_MixedOverlap_CorrectBehavior() + { + // Three detections: A and B overlap heavily, C is separate + var detections = new List + { + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.9f), // A + new(new PdfRectangle(10, 10, 110, 110), "text", 0, 0.8f), // B (overlaps A) + new(new PdfRectangle(500, 500, 600, 600), "text", 0, 0.7f) // C (no overlap) + }; + + var result = DetectionPostprocessing.ApplyNms(detections, 0.3f); + + Assert.Equal(2, result.Count); + Assert.Equal(0.9f, result[0].Confidence, 4); + Assert.Equal(0.7f, result[1].Confidence, 4); + } + + #endregion + + #region ScaleToPage + + [Fact] + public void ScaleToPage_SimpleScaling() + { + var det = new LayoutDetection(new PdfRectangle(0, 0, 320, 240), "text", 0, 0.9f); + var detections = new List { det }; + + var result = DetectionPostprocessing.ScaleToPage(detections, 640, 480, 1280, 960); + + // Scale 2x in both dimensions + Assert.Equal(0, result[0].BoundingBox.Left, 1); + Assert.Equal(640, result[0].BoundingBox.Right, 1); + Assert.Equal(0, result[0].BoundingBox.Bottom, 1); + Assert.Equal(480, result[0].BoundingBox.Top, 1); + } + + [Fact] + public void ScaleToPage_NullDetections_Throws() + { + Assert.Throws(() => + DetectionPostprocessing.ScaleToPage(null!, 640, 640, 100, 100)); + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ImagePreprocessingTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ImagePreprocessingTests.cs new file mode 100644 index 0000000..b4b9e05 --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/ImagePreprocessingTests.cs @@ -0,0 +1,391 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using SkiaSharp; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using Xunit; + +public class ImagePreprocessingTests +{ + #region ResizeExact + + [Fact] + public void ResizeExact_ProducesCorrectDimensions() + { + using var src = CreateSolidBitmap(100, 200, SKColors.Red); + using var result = ImagePreprocessing.ResizeExact(src, 50, 75); + + Assert.Equal(50, result.Width); + Assert.Equal(75, result.Height); + } + + [Fact] + public void ResizeExact_SquareImage() + { + using var src = CreateSolidBitmap(64, 64, SKColors.Blue); + using var result = ImagePreprocessing.ResizeExact(src, 640, 640); + + Assert.Equal(640, result.Width); + Assert.Equal(640, result.Height); + } + + [Fact] + public void ResizeExact_1x1Image() + { + using var src = CreateSolidBitmap(1, 1, SKColors.Green); + using var result = ImagePreprocessing.ResizeExact(src, 10, 10); + + Assert.Equal(10, result.Width); + Assert.Equal(10, result.Height); + } + + [Fact] + public void ResizeExact_Upscale() + { + using var src = CreateSolidBitmap(10, 10, SKColors.White); + using var result = ImagePreprocessing.ResizeExact(src, 640, 480); + + Assert.Equal(640, result.Width); + Assert.Equal(480, result.Height); + } + + [Fact] + public void ResizeExact_NullImage_Throws() + { + Assert.Throws(() => ImagePreprocessing.ResizeExact(null!, 10, 10)); + } + + #endregion + + #region Letterbox + + [Fact] + public void Letterbox_ProducesTargetDimensions() + { + using var src = CreateSolidBitmap(100, 200, SKColors.Red); + using var result = ImagePreprocessing.Letterbox(src, 640, 640, SKColors.Gray, out _, out _, out _); + + Assert.Equal(640, result.Width); + Assert.Equal(640, result.Height); + } + + [Fact] + public void Letterbox_PreservesAspectRatio_TallImage() + { + // Tall image (100×200): scale limited by height → 640/200=3.2 + using var src = CreateSolidBitmap(100, 200, SKColors.Red); + using var result = ImagePreprocessing.Letterbox(src, 640, 640, SKColors.Gray, out float scale, out int padX, out int padY); + + Assert.Equal(640 / 200f, scale, 3); + Assert.True(padX > 0, "Tall image should have horizontal padding"); + Assert.Equal(0, padY); + } + + [Fact] + public void Letterbox_PreservesAspectRatio_WideImage() + { + // Wide image (200×100): scale limited by width → 640/200=3.2 + using var src = CreateSolidBitmap(200, 100, SKColors.Blue); + using var result = ImagePreprocessing.Letterbox(src, 640, 640, SKColors.Gray, out float scale, out int padX, out int padY); + + Assert.Equal(640 / 200f, scale, 3); + Assert.Equal(0, padX); + Assert.True(padY > 0, "Wide image should have vertical padding"); + } + + [Fact] + public void Letterbox_SquareImage_NoPadding() + { + using var src = CreateSolidBitmap(100, 100, SKColors.Green); + using var result = ImagePreprocessing.Letterbox(src, 640, 640, SKColors.Gray, out float scale, out int padX, out int padY); + + Assert.Equal(640 / 100f, scale, 3); + Assert.Equal(0, padX); + Assert.Equal(0, padY); + } + + [Fact] + public void Letterbox_PaddingColor_Applied() + { + using var src = CreateSolidBitmap(100, 200, SKColors.Red); + using var result = ImagePreprocessing.Letterbox(src, 640, 640, SKColors.Gray, out _, out int padX, out _); + + // The top-left corner should be in the pad area (if padX > 0) + if (padX > 0) + { + var pixel = result.GetPixel(0, 0); + // Pad color should be gray + Assert.Equal(SKColors.Gray.Red, pixel.Red); + Assert.Equal(SKColors.Gray.Green, pixel.Green); + Assert.Equal(SKColors.Gray.Blue, pixel.Blue); + } + } + + [Fact] + public void Letterbox_NullImage_Throws() + { + Assert.Throws(() => + ImagePreprocessing.Letterbox(null!, 640, 640, SKColors.Gray, out _, out _, out _)); + } + + #endregion + + #region ToChwUint8 + + [Fact] + public void ToChwUint8_CorrectShape() + { + using var src = CreateSolidBitmap(4, 3, SKColors.Red); + var tensor = ImagePreprocessing.ToChwUint8(src); + + Assert.Equal(4, tensor.Dimensions.Length); // [1, 3, H, W] + Assert.Equal(1, tensor.Dimensions[0]); + Assert.Equal(3, tensor.Dimensions[1]); + Assert.Equal(3, tensor.Dimensions[2]); // height + Assert.Equal(4, tensor.Dimensions[3]); // width + } + + [Fact] + public void ToChwUint8_RedImage_CorrectChannelValues() + { + using var src = CreateSolidBitmap(2, 2, SKColors.Red); + var tensor = ImagePreprocessing.ToChwUint8(src); + + // Red channel = 255, Green = 0, Blue = 0 + Assert.Equal(255, tensor[0, 0, 0, 0]); // R + Assert.Equal(0, tensor[0, 1, 0, 0]); // G + Assert.Equal(0, tensor[0, 2, 0, 0]); // B + } + + [Fact] + public void ToChwUint8_WhiteImage_AllChannels255() + { + using var src = CreateSolidBitmap(2, 2, SKColors.White); + var tensor = ImagePreprocessing.ToChwUint8(src); + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + Assert.Equal(255, tensor[0, c, y, x]); + } + } + } + } + + [Fact] + public void ToChwUint8_1x1Image() + { + using var src = CreateSolidBitmap(1, 1, new SKColor(10, 20, 30)); + var tensor = ImagePreprocessing.ToChwUint8(src); + + Assert.Equal(1, tensor.Dimensions[2]); + Assert.Equal(1, tensor.Dimensions[3]); + Assert.Equal(10, tensor[0, 0, 0, 0]); + Assert.Equal(20, tensor[0, 1, 0, 0]); + Assert.Equal(30, tensor[0, 2, 0, 0]); + } + + [Fact] + public void ToChwUint8_NullImage_Throws() + { + Assert.Throws(() => ImagePreprocessing.ToChwUint8(null!)); + } + + #endregion + + #region ToChwFloat + + [Fact] + public void ToChwFloat_CorrectShape() + { + using var src = CreateSolidBitmap(5, 7, SKColors.Blue); + var tensor = ImagePreprocessing.ToChwFloat(src); + + Assert.Equal(4, tensor.Dimensions.Length); + Assert.Equal(1, tensor.Dimensions[0]); + Assert.Equal(3, tensor.Dimensions[1]); + Assert.Equal(7, tensor.Dimensions[2]); // height + Assert.Equal(5, tensor.Dimensions[3]); // width + } + + [Fact] + public void ToChwFloat_ValuesInZeroOneRange() + { + using var src = CreateSolidBitmap(3, 3, new SKColor(128, 64, 255)); + var tensor = ImagePreprocessing.ToChwFloat(src); + + for (int c = 0; c < 3; c++) + { + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < 3; x++) + { + float val = tensor[0, c, y, x]; + Assert.InRange(val, 0f, 1f); + } + } + } + } + + [Fact] + public void ToChwFloat_WhiteImage_AllOnes() + { + using var src = CreateSolidBitmap(2, 2, SKColors.White); + var tensor = ImagePreprocessing.ToChwFloat(src); + + for (int c = 0; c < 3; c++) + { + Assert.Equal(1f, tensor[0, c, 0, 0], 4); + } + } + + [Fact] + public void ToChwFloat_BlackImage_AllZeros() + { + using var src = CreateSolidBitmap(2, 2, SKColors.Black); + var tensor = ImagePreprocessing.ToChwFloat(src); + + for (int c = 0; c < 3; c++) + { + Assert.Equal(0f, tensor[0, c, 0, 0], 4); + } + } + + [Fact] + public void ToChwFloat_SpecificColor_CorrectNormalization() + { + using var src = CreateSolidBitmap(1, 1, new SKColor(128, 0, 255)); + var tensor = ImagePreprocessing.ToChwFloat(src); + + Assert.Equal(128f / 255f, tensor[0, 0, 0, 0], 3); // R + Assert.Equal(0f, tensor[0, 1, 0, 0], 3); // G + Assert.Equal(1f, tensor[0, 2, 0, 0], 3); // B + } + + [Fact] + public void ToChwFloat_NullImage_Throws() + { + Assert.Throws(() => ImagePreprocessing.ToChwFloat(null!)); + } + + #endregion + + #region NormalizeImageNet + + [Fact] + public void NormalizeImageNet_AppliesCorrectTransformation() + { + int w = 2, h = 2; + int channelSize = w * h; + var chw = new float[3 * channelSize]; + + // Fill with 0.5 for all channels + Array.Fill(chw, 0.5f); + + ImagePreprocessing.NormalizeImageNet(chw, w, h); + + // Expected: (0.5 - mean) / std per channel + float[] mean = [0.485f, 0.456f, 0.406f]; + float[] std = [0.229f, 0.224f, 0.225f]; + + for (int c = 0; c < 3; c++) + { + float expected = (0.5f - mean[c]) / std[c]; + for (int i = 0; i < channelSize; i++) + { + Assert.Equal(expected, chw[c * channelSize + i], 4); + } + } + } + + [Fact] + public void NormalizeImageNet_ZeroInput_NegativeMeanDividedByStd() + { + int w = 1, h = 1; + var chw = new float[3]; + + ImagePreprocessing.NormalizeImageNet(chw, w, h); + + Assert.Equal(-0.485f / 0.229f, chw[0], 4); + Assert.Equal(-0.456f / 0.224f, chw[1], 4); + Assert.Equal(-0.406f / 0.225f, chw[2], 4); + } + + #endregion + + #region Normalize (custom) + + [Fact] + public void Normalize_CustomMeanStd() + { + int w = 2, h = 1; + var chw = new float[6]; // 3 channels × 2 pixels + Array.Fill(chw, 1.0f); + + float[] mean = [0.5f, 0.5f, 0.5f]; + float[] std = [0.5f, 0.5f, 0.5f]; + ImagePreprocessing.Normalize(chw, w, h, mean, std); + + // (1.0 - 0.5) / 0.5 = 1.0 + for (int i = 0; i < 6; i++) + { + Assert.Equal(1.0f, chw[i], 4); + } + } + + [Fact] + public void Normalize_IdentityTransform() + { + int w = 1, h = 1; + var chw = new float[] { 0.3f, 0.6f, 0.9f }; + + float[] mean = [0f, 0f, 0f]; + float[] std = [1f, 1f, 1f]; + ImagePreprocessing.Normalize(chw, w, h, mean, std); + + // (x - 0) / 1 = x, unchanged + Assert.Equal(0.3f, chw[0], 4); + Assert.Equal(0.6f, chw[1], 4); + Assert.Equal(0.9f, chw[2], 4); + } + + [Fact] + public void Normalize_InvalidMeanLength_Throws() + { + var chw = new float[3]; + float[] badMean = [0.5f, 0.5f]; // length 2 instead of 3 + float[] std = [1f, 1f, 1f]; + + Assert.Throws(() => + ImagePreprocessing.Normalize(chw, 1, 1, badMean, std)); + } + + [Fact] + public void Normalize_InvalidStdLength_Throws() + { + var chw = new float[3]; + float[] mean = [0.5f, 0.5f, 0.5f]; + float[] badStd = [1f]; // length 1 instead of 3 + + Assert.Throws(() => + ImagePreprocessing.Normalize(chw, 1, 1, mean, badStd)); + } + + #endregion + + #region Helpers + + private static SKBitmap CreateSolidBitmap(int width, int height, SKColor color) + { + var info = new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + var bitmap = new SKBitmap(info); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(color); + return bitmap; + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/LayoutModelOptionsTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/LayoutModelOptionsTests.cs new file mode 100644 index 0000000..90bb4a2 --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/LayoutModelOptionsTests.cs @@ -0,0 +1,241 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; +using Xunit; + +public class LayoutModelOptionsTests +{ + #region Default values + + [Fact] + public void DefaultOptions_InputWidth() + { + var options = new LayoutModelOptions(); + Assert.Equal(640, options.InputWidth); + } + + [Fact] + public void DefaultOptions_InputHeight() + { + var options = new LayoutModelOptions(); + Assert.Equal(640, options.InputHeight); + } + + [Fact] + public void DefaultOptions_ResizeMode() + { + var options = new LayoutModelOptions(); + Assert.Equal(ResizeMode.Exact, options.Resize); + } + + [Fact] + public void DefaultOptions_PixelFormat() + { + var options = new LayoutModelOptions(); + Assert.Equal(PixelFormat.Uint8Chw, options.PixelFormat); + } + + [Fact] + public void DefaultOptions_NormalizationIsNull() + { + var options = new LayoutModelOptions(); + Assert.Null(options.Normalization); + } + + [Fact] + public void DefaultOptions_ConfidenceThreshold() + { + var options = new LayoutModelOptions(); + Assert.Equal(0.3f, options.ConfidenceThreshold, 4); + } + + [Fact] + public void DefaultOptions_NmsIouThreshold() + { + var options = new LayoutModelOptions(); + Assert.Equal(0.45f, options.NmsIouThreshold, 4); + } + + [Fact] + public void DefaultOptions_RequiresNmsFalse() + { + var options = new LayoutModelOptions(); + Assert.False(options.RequiresNms); + } + + [Fact] + public void DefaultOptions_OutputBboxFormat() + { + var options = new LayoutModelOptions(); + Assert.Equal(BboxFormat.CxCyWh, options.OutputBboxFormat); + } + + [Fact] + public void DefaultOptions_ClassLabelsNull() + { + var options = new LayoutModelOptions(); + Assert.Null(options.ClassLabels); + } + + #endregion + + #region Custom init values + + [Fact] + public void Options_CustomValues() + { + var labels = new Dictionary { [0] = "text", [1] = "image" }; + var options = new LayoutModelOptions + { + InputWidth = 1024, + InputHeight = 768, + Resize = ResizeMode.Letterbox, + PixelFormat = PixelFormat.Float32Chw, + Normalization = WellKnownNormalizations.ImageNet, + ConfidenceThreshold = 0.5f, + NmsIouThreshold = 0.6f, + RequiresNms = true, + OutputBboxFormat = BboxFormat.Xyxy, + ClassLabels = labels + }; + + Assert.Equal(1024, options.InputWidth); + Assert.Equal(768, options.InputHeight); + Assert.Equal(ResizeMode.Letterbox, options.Resize); + Assert.Equal(PixelFormat.Float32Chw, options.PixelFormat); + Assert.NotNull(options.Normalization); + Assert.Equal(0.5f, options.ConfidenceThreshold, 4); + Assert.Equal(0.6f, options.NmsIouThreshold, 4); + Assert.True(options.RequiresNms); + Assert.Equal(BboxFormat.Xyxy, options.OutputBboxFormat); + Assert.Equal(2, options.ClassLabels!.Count); + } + + #endregion + + #region WellKnownNormalizations + + [Fact] + public void ImageNet_HasCorrectMean() + { + var norm = WellKnownNormalizations.ImageNet; + + Assert.Equal(3, norm.Mean.Length); + Assert.Equal(0.485f, norm.Mean[0], 4); + Assert.Equal(0.456f, norm.Mean[1], 4); + Assert.Equal(0.406f, norm.Mean[2], 4); + } + + [Fact] + public void ImageNet_HasCorrectStd() + { + var norm = WellKnownNormalizations.ImageNet; + + Assert.Equal(3, norm.Std.Length); + Assert.Equal(0.229f, norm.Std[0], 4); + Assert.Equal(0.224f, norm.Std[1], 4); + Assert.Equal(0.225f, norm.Std[2], 4); + } + + [Fact] + public void ZeroToOne_HasCorrectMean() + { + var norm = WellKnownNormalizations.ZeroToOne; + + Assert.Equal(3, norm.Mean.Length); + Assert.Equal(0f, norm.Mean[0], 4); + Assert.Equal(0f, norm.Mean[1], 4); + Assert.Equal(0f, norm.Mean[2], 4); + } + + [Fact] + public void ZeroToOne_HasCorrectStd() + { + var norm = WellKnownNormalizations.ZeroToOne; + + Assert.Equal(3, norm.Std.Length); + float expected = 1f / 255f; + Assert.Equal(expected, norm.Std[0], 6); + Assert.Equal(expected, norm.Std[1], 6); + Assert.Equal(expected, norm.Std[2], 6); + } + + #endregion + + #region Enums + + [Theory] + [InlineData(ResizeMode.Exact)] + [InlineData(ResizeMode.Letterbox)] + [InlineData(ResizeMode.AspectPreserve)] + public void ResizeMode_AllValuesExist(ResizeMode mode) + { + Assert.True(Enum.IsDefined(mode)); + } + + [Theory] + [InlineData(PixelFormat.Uint8Chw)] + [InlineData(PixelFormat.Float32Chw)] + public void PixelFormat_AllValuesExist(PixelFormat format) + { + Assert.True(Enum.IsDefined(format)); + } + + [Theory] + [InlineData(BboxFormat.CxCyWh)] + [InlineData(BboxFormat.Xyxy)] + [InlineData(BboxFormat.Xywh)] + public void BboxFormat_AllValuesExist(BboxFormat format) + { + Assert.True(Enum.IsDefined(format)); + } + + [Fact] + public void ResizeMode_HasThreeValues() + { + Assert.Equal(3, Enum.GetValues().Length); + } + + [Fact] + public void PixelFormat_HasTwoValues() + { + Assert.Equal(2, Enum.GetValues().Length); + } + + [Fact] + public void BboxFormat_HasThreeValues() + { + Assert.Equal(3, Enum.GetValues().Length); + } + + #endregion + + #region ImageNormalization record + + [Fact] + public void ImageNormalization_StoresMeanAndStd() + { + float[] mean = [0.1f, 0.2f, 0.3f]; + float[] std = [0.4f, 0.5f, 0.6f]; + + var norm = new ImageNormalization(mean, std); + + Assert.Same(mean, norm.Mean); + Assert.Same(std, norm.Std); + } + + [Fact] + public void ImageNormalization_RecordEquality() + { + float[] mean = [0.5f, 0.5f, 0.5f]; + float[] std = [1f, 1f, 1f]; + + var a = new ImageNormalization(mean, std); + var b = new ImageNormalization(mean, std); + + Assert.Equal(a, b); + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterDiTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterDiTests.cs new file mode 100644 index 0000000..60e663b --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterDiTests.cs @@ -0,0 +1,141 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.ML.OnnxRuntime; +using SkiaSharp; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using Xunit; + +public class OnnxPageSegmenterDiTests +{ + #region Helpers + + private sealed class StubModel : ILayoutDetectionModel + { + public string ModelPath => "stub.onnx"; + + public IReadOnlyDictionary LabelMapping { get; } = new Dictionary(); + + public IReadOnlyList Preprocess(SKBitmap p, int w, int h) + { + return Array.Empty(); + } + + public IReadOnlyList Postprocess( + IDisposableReadOnlyCollection r, int w, int h) + { + return Array.Empty(); + } + + public void Dispose() { } + } + + #endregion + + #region Service registration + + [Fact] + public void AddOnnxPageSegmenter_RegistersILayoutDetectionModel() + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(); + var provider = services.BuildServiceProvider(); + + var model = provider.GetService(); + + Assert.NotNull(model); + Assert.IsType(model); + } + + [Fact] + public void AddOnnxPageSegmenter_RegistersIPageSegmenter() + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(); + + var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IPageSegmenter)); + + Assert.NotNull(descriptor); + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + } + + [Fact] + public void AddOnnxPageSegmenter_IdempotentRegistration() + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(); + services.AddOnnxPageSegmenter(); + + int modelCount = services.Count(d => d.ServiceType == typeof(ILayoutDetectionModel)); + int segmenterCount = services.Count(d => d.ServiceType == typeof(OnnxPageSegmenter)); + int pageSegmenterCount = services.Count(d => d.ServiceType == typeof(IPageSegmenter)); + + Assert.Equal(1, modelCount); + Assert.Equal(1, segmenterCount); + Assert.Equal(1, pageSegmenterCount); + } + + #endregion + + #region Options configuration + + [Fact] + public void AddOnnxPageSegmenter_ConfigureAppliesOptions() + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(o => + { + o.ConfidenceThreshold = 0.8f; + o.RenderDpi = 300; + }); + var provider = services.BuildServiceProvider(); + + var opts = provider.GetRequiredService>().Value; + + Assert.Equal(0.8f, opts.ConfidenceThreshold, 4); + Assert.Equal(300, opts.RenderDpi); + } + + [Fact] + public void AddOnnxPageSegmenter_DefaultOptionsWithoutConfigure() + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(); + var provider = services.BuildServiceProvider(); + + var opts = provider.GetRequiredService>().Value; + + Assert.Equal(0.3f, opts.ConfidenceThreshold, 4); + Assert.Equal(150, opts.RenderDpi); + Assert.Null(opts.SessionOptions); + } + + [Fact] + public void AddOnnxPageSegmenter_InvalidOptions_ThrowsOnResolve() + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(o => o.ConfidenceThreshold = -1f); + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>(); + + Assert.Throws(() => options.Value); + } + + #endregion + + #region Constructor null checks + + [Fact] + public void IOptionsCtor_NullOptions_Throws() + { + var model = new StubModel(); + + Assert.Throws( + () => new OnnxPageSegmenter(model, (IOptions)null!)); + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterTests.cs new file mode 100644 index 0000000..a89c88c --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxPageSegmenterTests.cs @@ -0,0 +1,311 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using Microsoft.ML.OnnxRuntime; +using SkiaSharp; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.Core; +using UglyToad.PdfPig.DocumentLayoutAnalysis; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using UglyToad.PdfPig.PdfFonts; +using Xunit; + +public class OnnxPageSegmenterTests +{ + #region Constructor + + [Fact] + public void Constructor_NullModel_Throws() + { + Assert.Throws(() => new OnnxPageSegmenter(null!)); + } + + #endregion + + #region IDisposable + + [Fact] + public void Dispose_CanBeCalledOnStubModel() + { + // RtDetrLayoutModel doesn't create a session in its constructor, + // so we can construct it with a fake path for disposal testing. + var model = new StubLayoutModel(); + // OnnxPageSegmenter creates an InferenceSession in its constructor + // which requires a valid model file. We test dispose via the stub model directly. + model.Dispose(); + + // Calling Dispose again should not throw + model.Dispose(); + } + + [Fact] + public void StubModel_ImplementsIDisposable() + { + var model = new StubLayoutModel(); + Assert.IsAssignableFrom(model); + model.Dispose(); + } + + #endregion + + #region GetBlocks - empty words + + [SkippableFact] + public void GetBlocks_EmptyWords_ReturnsEmpty() + { + // This test requires a real ONNX model file + string modelPath = GetTestModelPath(); + Skip.IfNot(File.Exists(modelPath), "ONNX model file not found; skipping integration test."); + + using var model = new StubLayoutModelWithFile(modelPath); + using var segmenter = new OnnxPageSegmenter(model); + + var result = segmenter.GetBlocks(new List()); + Assert.Empty(result); + } + + #endregion + + #region Detection-to-TextBlock mapping logic (via DetectionPostprocessing + synthetic data) + + [Fact] + public void DetectionOverlap_WordInsideDetection_Captured() + { + // Verify the overlap logic: a word fully inside a detection box + var detBox = new PdfRectangle(0, 0, 100, 100); + var wordBox = new PdfRectangle(10, 10, 50, 50); + + // Verify overlap exists (using ComputeIoU as proxy) + float iou = DetectionPostprocessing.ComputeIoU(detBox, wordBox); + Assert.True(iou > 0, "Word inside detection should overlap"); + } + + [Fact] + public void DetectionOverlap_WordOutsideDetection_NotCaptured() + { + var detBox = new PdfRectangle(0, 0, 100, 100); + var wordBox = new PdfRectangle(200, 200, 250, 250); + + float iou = DetectionPostprocessing.ComputeIoU(detBox, wordBox); + Assert.Equal(0f, iou, 4); + } + + [Fact] + public void ConfidenceFiltering_BelowThreshold_NotIncluded() + { + var detections = new List + { + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.1f), // below 0.3 threshold + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.5f), // above threshold + }; + + // Simulate filtering as OnnxPageSegmenter does + float threshold = 0.3f; + var filtered = detections.Where(d => d.Confidence >= threshold).ToList(); + + Assert.Single(filtered); + Assert.Equal(0.5f, filtered[0].Confidence, 4); + } + + [Fact] + public void ConfidenceFiltering_AllBelowThreshold_ReturnsNone() + { + var detections = new List + { + new(new PdfRectangle(0, 0, 100, 100), "text", 0, 0.1f), + new(new PdfRectangle(50, 50, 150, 150), "table", 1, 0.2f), + }; + + float threshold = 0.3f; + var filtered = detections.Where(d => d.Confidence >= threshold).ToList(); + + Assert.Empty(filtered); + } + + #endregion + + #region Synthetic Word creation and TextBlock composition + + [Fact] + public void SyntheticWord_CanBeCreated() + { + var word = CreateWord(new PdfRectangle(10, 10, 50, 25)); + Assert.NotNull(word); + Assert.Equal("a", word.Text); + } + + [Fact] + public void SyntheticTextBlock_FromMultipleWords() + { + var w1 = CreateWord(new PdfRectangle(10, 10, 50, 25)); + var w2 = CreateWord(new PdfRectangle(60, 10, 100, 25)); + + var line = new TextLine(new[] { w1, w2 }); + var block = new TextBlock(new[] { line }); + + Assert.Equal(2, block.TextLines[0].Words.Count); + } + + [Fact] + public void WordsOnDifferentLines_GroupedCorrectly() + { + // Two words on different vertical positions + var w1 = CreateWord(new PdfRectangle(10, 100, 50, 120)); + var w2 = CreateWord(new PdfRectangle(10, 50, 50, 70)); + + var line1 = new TextLine(new[] { w1 }); + var line2 = new TextLine(new[] { w2 }); + var block = new TextBlock(new[] { line1, line2 }); + + Assert.Equal(2, block.TextLines.Count); + } + + #endregion + + #region LayoutDetection record + + [Fact] + public void LayoutDetection_RecordProperties() + { + var box = new PdfRectangle(10, 20, 100, 80); + var det = new LayoutDetection(box, "table", 5, 0.95f); + + Assert.Equal(box, det.BoundingBox); + Assert.Equal("table", det.Label); + Assert.Equal(5, det.ClassId); + Assert.Equal(0.95f, det.Confidence, 4); + } + + [Fact] + public void LayoutDetection_WithExpression() + { + var det = new LayoutDetection(new PdfRectangle(0, 0, 10, 10), "text", 0, 0.5f); + var updated = det with { Confidence = 0.9f }; + + Assert.Equal(0.9f, updated.Confidence, 4); + Assert.Equal("text", updated.Label); + } + + #endregion + + #region OnnxSegmenterOptions + + [Fact] + public void OnnxSegmenterOptions_DefaultValues() + { + var opts = new OnnxSegmenterOptions(); + Assert.Equal(0.3f, opts.ConfidenceThreshold, 4); + Assert.Equal(150, opts.RenderDpi); + Assert.Null(opts.SessionOptions); + } + + [Fact] + public void OnnxSegmenterOptions_CustomValues() + { + var sessionOpts = new SessionOptions(); + var opts = new OnnxSegmenterOptions + { + ConfidenceThreshold = 0.7f, + RenderDpi = 300, + SessionOptions = sessionOpts + }; + + Assert.Equal(0.7f, opts.ConfidenceThreshold, 4); + Assert.Equal(300, opts.RenderDpi); + Assert.Same(sessionOpts, opts.SessionOptions); + sessionOpts.Dispose(); + } + + #endregion + + #region Helpers + + private static Word CreateWord(PdfRectangle boundingBox) + { + var letter = new Letter( + "a", + boundingBox, + boundingBox, + boundingBox.BottomLeft, + boundingBox.BottomRight, + 10, 1, + (FontDetails)null!, + TextRenderingMode.NeitherClip, + null!, null!, + 0, 0); + return new Word(new[] { letter }); + } + + private static string GetTestModelPath() + { + // Look for model in common locations + var candidates = new[] + { + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "models", "rtdetr.onnx"), + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rtdetr.onnx"), + }; + return candidates.FirstOrDefault(File.Exists) ?? candidates[0]; + } + + /// + /// A minimal stub implementing ILayoutDetectionModel for unit testing without an ONNX model file. + /// + private sealed class StubLayoutModel : ILayoutDetectionModel + { + public string ModelPath => "stub_model.onnx"; + + public IReadOnlyDictionary LabelMapping { get; } = new Dictionary + { + [0] = "text", + [1] = "table" + }; + + public IReadOnlyList Preprocess(SKBitmap pageImage, int originalWidth, int originalHeight) + { + return Array.Empty(); + } + + public IReadOnlyList Postprocess( + IDisposableReadOnlyCollection results, + int originalWidth, int originalHeight) + { + return Array.Empty(); + } + + public void Dispose() { } + } + + /// + /// A stub layout model that accepts an actual model file path for integration tests. + /// + private sealed class StubLayoutModelWithFile : ILayoutDetectionModel + { + public StubLayoutModelWithFile(string modelPath) + { + ModelPath = modelPath; + } + + public string ModelPath { get; } + + public IReadOnlyDictionary LabelMapping { get; } = new Dictionary + { + [0] = "text" + }; + + public IReadOnlyList Preprocess(SKBitmap pageImage, int originalWidth, int originalHeight) + { + return Array.Empty(); + } + + public IReadOnlyList Postprocess( + IDisposableReadOnlyCollection results, + int originalWidth, int originalHeight) + { + return Array.Empty(); + } + + public void Dispose() { } + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxSegmenterOptionsValidatorTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxSegmenterOptionsValidatorTests.cs new file mode 100644 index 0000000..3e4146a --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/OnnxSegmenterOptionsValidatorTests.cs @@ -0,0 +1,129 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.ML.OnnxRuntime; +using SkiaSharp; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using Xunit; + +public class OnnxSegmenterOptionsValidatorTests +{ + #region Helpers + + private static IOptions BuildOptions(Action? configure = null) + { + var services = new ServiceCollection(); + services.AddOnnxPageSegmenter(configure); + var provider = services.BuildServiceProvider(); + return provider.GetRequiredService>(); + } + + private sealed class StubModel : ILayoutDetectionModel + { + public string ModelPath => "stub.onnx"; + + public IReadOnlyDictionary LabelMapping { get; } = new Dictionary(); + + public IReadOnlyList Preprocess(SKBitmap p, int w, int h) + { + return Array.Empty(); + } + + public IReadOnlyList Postprocess( + IDisposableReadOnlyCollection r, int w, int h) + { + return Array.Empty(); + } + + public void Dispose() { } + } + + #endregion + + #region ConfidenceThreshold + + [Fact] + public void DefaultOptions_Succeeds() + { + var options = BuildOptions(); + + var opts = options.Value; + + Assert.Equal(0.3f, opts.ConfidenceThreshold, 4); + Assert.Equal(150, opts.RenderDpi); + } + + [Fact] + public void ConfidenceThreshold_Negative_Fails() + { + var options = BuildOptions(o => o.ConfidenceThreshold = -0.1f); + + var ex = Assert.Throws(() => options.Value); + Assert.Contains("ConfidenceThreshold", ex.Message); + } + + [Fact] + public void ConfidenceThreshold_AboveOne_Fails() + { + var options = BuildOptions(o => o.ConfidenceThreshold = 1.1f); + + var ex = Assert.Throws(() => options.Value); + Assert.Contains("ConfidenceThreshold", ex.Message); + } + + [Fact] + public void ConfidenceThreshold_ExactlyZero_Succeeds() + { + var options = BuildOptions(o => o.ConfidenceThreshold = 0f); + + var opts = options.Value; + + Assert.Equal(0f, opts.ConfidenceThreshold, 4); + } + + [Fact] + public void ConfidenceThreshold_ExactlyOne_Succeeds() + { + var options = BuildOptions(o => o.ConfidenceThreshold = 1f); + + var opts = options.Value; + + Assert.Equal(1f, opts.ConfidenceThreshold, 4); + } + + #endregion + + #region RenderDpi + + [Fact] + public void RenderDpi_Zero_Fails() + { + var options = BuildOptions(o => o.RenderDpi = 0); + + var ex = Assert.Throws(() => options.Value); + Assert.Contains("RenderDpi", ex.Message); + } + + [Fact] + public void RenderDpi_Negative_Fails() + { + var options = BuildOptions(o => o.RenderDpi = -1); + + var ex = Assert.Throws(() => options.Value); + Assert.Contains("RenderDpi", ex.Message); + } + + [Fact] + public void RenderDpi_One_Succeeds() + { + var options = BuildOptions(o => o.RenderDpi = 1); + + var opts = options.Value; + + Assert.Equal(1, opts.RenderDpi); + } + + #endregion +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PageImageRendererTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PageImageRendererTests.cs new file mode 100644 index 0000000..0c7c864 --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PageImageRendererTests.cs @@ -0,0 +1,94 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using System.Collections.Generic; +using SkiaSharp; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.Core; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using UglyToad.PdfPig.PdfFonts; +using Xunit; + +public class PageImageRendererTests +{ + [Fact] + public void RenderWords_NullWords_ThrowsArgumentNullException() + { + Assert.Throws(() => + PageImageRenderer.RenderWords(null!, 595, 842)); + } + + [Fact] + public void RenderWords_DefaultDpi_CorrectBitmapDimensions() + { + using var bitmap = PageImageRenderer.RenderWords( + new List(), 595, 842); + + Assert.Equal(1239, bitmap.Width); + Assert.Equal(1754, bitmap.Height); + } + + [Fact] + public void RenderWords_CustomDpi_ScalesDimensions() + { + using var bitmap = PageImageRenderer.RenderWords( + new List(), 595, 842, dpi: 72); + + Assert.Equal(595, bitmap.Width); + Assert.Equal(842, bitmap.Height); + } + + [Fact] + public void RenderWords_EmptyWordsList_ReturnsWhiteBitmap() + { + using var bitmap = PageImageRenderer.RenderWords( + new List(), 595, 842); + + Assert.NotNull(bitmap); + Assert.Equal(1239, bitmap.Width); + Assert.Equal(1754, bitmap.Height); + Assert.Equal(SKColors.White, bitmap.GetPixel(0, 0)); + } + + [Fact] + public void RenderWords_WithWords_RendersNonWhitePixels() + { + var words = new List + { + CreateWord(new PdfRectangle(10, 10, 100, 30)) + }; + + using var bitmap = PageImageRenderer.RenderWords(words, 200, 200, dpi: 72); + + // The word occupies a region — at least one pixel should be non-white (black fill) + bool hasNonWhite = false; + for (int y = 0; y < bitmap.Height && !hasNonWhite; y++) + { + for (int x = 0; x < bitmap.Width && !hasNonWhite; x++) + { + if (bitmap.GetPixel(x, y) != SKColors.White) + { + hasNonWhite = true; + } + } + } + + Assert.True(hasNonWhite, "Expected at least one non-white pixel from rendered word bounding box."); + } + + private static Word CreateWord(PdfRectangle boundingBox) + { + var letter = new Letter( + "a", + boundingBox, + boundingBox, + boundingBox.BottomLeft, + boundingBox.BottomRight, + 10, 1, + (FontDetails)null!, + TextRenderingMode.NeitherClip, + null!, null!, + 0, 0); + return new Word(new[] { letter }); + } +} +#endif diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj new file mode 100644 index 0000000..9512caa --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj @@ -0,0 +1,35 @@ + + + + CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests + CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests + net10.0 + true + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/RtDetrLayoutModelTests.cs b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/RtDetrLayoutModelTests.cs new file mode 100644 index 0000000..e708dfd --- /dev/null +++ b/MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/RtDetrLayoutModelTests.cs @@ -0,0 +1,154 @@ +#if NET8_0_OR_GREATER +namespace CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.UnitTests; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis; +using CommunityToolkit.DocumentProcessing.PdfPig.OnnxLayoutAnalysis.Models; +using Xunit; + +public class RtDetrLayoutModelTests +{ + #region Constructor + + [Fact] + public void Constructor_StoresModelPath() + { + using var model = new RtDetrLayoutModel("test_model.onnx"); + Assert.Equal("test_model.onnx", model.ModelPath); + } + + [Fact] + public void Constructor_NullModelPath_Throws() + { + Assert.Throws(() => new RtDetrLayoutModel(null!)); + } + + #endregion + + #region LabelMapping + + [Fact] + public void LabelMapping_Has17Entries() + { + using var model = new RtDetrLayoutModel("test.onnx"); + Assert.Equal(17, model.LabelMapping.Count); + } + + [Fact] + public void LabelMapping_ContainsExpectedLabels() + { + using var model = new RtDetrLayoutModel("test.onnx"); + var mapping = model.LabelMapping; + + Assert.Equal("caption", mapping[0]); + Assert.Equal("footnote", mapping[1]); + Assert.Equal("formula", mapping[2]); + Assert.Equal("list_item", mapping[3]); + Assert.Equal("page_footer", mapping[4]); + Assert.Equal("page_header", mapping[5]); + Assert.Equal("picture", mapping[6]); + Assert.Equal("section_header", mapping[7]); + Assert.Equal("table", mapping[8]); + Assert.Equal("text", mapping[9]); + Assert.Equal("title", mapping[10]); + Assert.Equal("document_index", mapping[11]); + Assert.Equal("code", mapping[12]); + Assert.Equal("checkbox_selected", mapping[13]); + Assert.Equal("checkbox_unselected", mapping[14]); + Assert.Equal("form", mapping[15]); + Assert.Equal("key_value_region", mapping[16]); + } + + [Fact] + public void LabelMapping_ContiguousKeys_0To16() + { + using var model = new RtDetrLayoutModel("test.onnx"); + var mapping = model.LabelMapping; + + for (int i = 0; i <= 16; i++) + { + Assert.True(mapping.ContainsKey(i), $"Missing key {i}"); + } + } + + [Fact] + public void LabelMapping_AllValuesNonEmpty() + { + using var model = new RtDetrLayoutModel("test.onnx"); + + foreach (var kvp in model.LabelMapping) + { + Assert.False(string.IsNullOrWhiteSpace(kvp.Value), + $"Label for key {kvp.Key} should not be empty"); + } + } + + #endregion + + #region ILayoutDetectionModel interface + + [Fact] + public void ImplementsILayoutDetectionModel() + { + using var model = new RtDetrLayoutModel("test.onnx"); + Assert.IsAssignableFrom(model); + } + + [Fact] + public void ImplementsIDisposable() + { + using var model = new RtDetrLayoutModel("test.onnx"); + Assert.IsAssignableFrom(model); + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + var model = new RtDetrLayoutModel("test.onnx"); + model.Dispose(); + model.Dispose(); // Should not throw + } + + #endregion + + #region Preprocess (requires model file) + + [SkippableFact] + public void Preprocess_NullImage_Throws() + { + string modelPath = FindModelPath(); + Skip.IfNot(File.Exists(modelPath), "ONNX model file not found; skipping."); + + using var model = new RtDetrLayoutModel(modelPath); + Assert.Throws(() => model.Preprocess(null!, 100, 100)); + } + + #endregion + + #region Postprocess (requires model file) + + [SkippableFact] + public void Postprocess_NullResults_Throws() + { + string modelPath = FindModelPath(); + Skip.IfNot(File.Exists(modelPath), "ONNX model file not found; skipping."); + + using var model = new RtDetrLayoutModel(modelPath); + Assert.Throws(() => model.Postprocess(null!, 640, 640)); + } + + #endregion + + #region Helpers + + private static string FindModelPath() + { + var candidates = new[] + { + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "models", "rtdetr.onnx"), + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "rtdetr.onnx"), + }; + return candidates.FirstOrDefault(File.Exists) ?? candidates[0]; + } + + #endregion +} +#endif diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..1f5c7e6 --- /dev/null +++ b/nuget.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + From 518d8f62590bd47f66d310d5b8ef3ca92eaf77ee Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Mon, 4 May 2026 21:45:30 -0400 Subject: [PATCH 2/7] Add CommunityToolkit.DataRetrieval and OnnxReranker packages Port retrieval processors from MEDIExtensions to CommunityToolkit: - DataRetrieval: MultiQueryExpander, HydeQueryTransformer, AdaptiveRouter, CragValidator, LlmReranker, TreeSearchRetriever, SelfRagOrchestrator, SpeculativeRagOrchestrator, RetrievalPipelineBuilder, DI extensions - DataRetrieval.OnnxReranker: CrossEncoderReranker with ONNX inference, TextPairTokenizer, TokenizerFactory (BERT/BPE/SentencePiece support) Both packages target net10.0;net8.0, build with 0 errors/0 warnings. Added local NuGet source for Microsoft.Extensions.DataRetrieval packages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Packages.props | 4 + MEDI/MEDI.slnf | 2 + .../CrossEncoderReranker.cs | 156 +++++++++++ .../CrossEncoderRerankerOptions.cs | 43 +++ .../DataRetrieval.OnnxReranker.csproj | 24 ++ .../Internal/OnnxModelSession.cs | 186 +++++++++++++ .../Internal/TextPairTokenizer.cs | 110 ++++++++ .../Internal/TokenizerFactory.cs | 248 ++++++++++++++++++ MEDI/src/DataRetrieval/AdaptiveRouter.cs | 86 ++++++ MEDI/src/DataRetrieval/CragValidator.cs | 119 +++++++++ MEDI/src/DataRetrieval/DataRetrieval.csproj | 26 ++ ...ataRetrievalServiceCollectionExtensions.cs | 72 +++++ .../src/DataRetrieval/HydeQueryTransformer.cs | 58 ++++ MEDI/src/DataRetrieval/JsonDefaults.cs | 13 + MEDI/src/DataRetrieval/LlmReranker.cs | 108 ++++++++ MEDI/src/DataRetrieval/MultiQueryExpander.cs | 75 ++++++ .../DataRetrieval/RetrievalPipelineBuilder.cs | 145 ++++++++++ MEDI/src/DataRetrieval/SelfRagOrchestrator.cs | 231 ++++++++++++++++ .../SpeculativeRagOrchestrator.cs | 219 ++++++++++++++++ MEDI/src/DataRetrieval/TreeSearchRetriever.cs | 70 +++++ nuget.config | 4 + 21 files changed, 1999 insertions(+) create mode 100644 MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderReranker.cs create mode 100644 MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderRerankerOptions.cs create mode 100644 MEDI/src/DataRetrieval.OnnxReranker/DataRetrieval.OnnxReranker.csproj create mode 100644 MEDI/src/DataRetrieval.OnnxReranker/Internal/OnnxModelSession.cs create mode 100644 MEDI/src/DataRetrieval.OnnxReranker/Internal/TextPairTokenizer.cs create mode 100644 MEDI/src/DataRetrieval.OnnxReranker/Internal/TokenizerFactory.cs create mode 100644 MEDI/src/DataRetrieval/AdaptiveRouter.cs create mode 100644 MEDI/src/DataRetrieval/CragValidator.cs create mode 100644 MEDI/src/DataRetrieval/DataRetrieval.csproj create mode 100644 MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs create mode 100644 MEDI/src/DataRetrieval/HydeQueryTransformer.cs create mode 100644 MEDI/src/DataRetrieval/JsonDefaults.cs create mode 100644 MEDI/src/DataRetrieval/LlmReranker.cs create mode 100644 MEDI/src/DataRetrieval/MultiQueryExpander.cs create mode 100644 MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs create mode 100644 MEDI/src/DataRetrieval/SelfRagOrchestrator.cs create mode 100644 MEDI/src/DataRetrieval/SpeculativeRagOrchestrator.cs create mode 100644 MEDI/src/DataRetrieval/TreeSearchRetriever.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7968ba1..768fdb1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,8 +25,12 @@ + + + + diff --git a/MEDI/MEDI.slnf b/MEDI/MEDI.slnf index 3330c32..042c63f 100644 --- a/MEDI/MEDI.slnf +++ b/MEDI/MEDI.slnf @@ -5,6 +5,8 @@ [ "MEDI/src/PdfPig.OnnxLayoutAnalysis/PdfPig.OnnxLayoutAnalysis.csproj", "MEDI/src/PdfPig.DataIngestion/PdfPig.DataIngestion.csproj", + "MEDI/src/DataRetrieval/DataRetrieval.csproj", + "MEDI/src/DataRetrieval.OnnxReranker/DataRetrieval.OnnxReranker.csproj", "MEDI/test/PdfPig.OnnxLayoutAnalysis.UnitTests/PdfPig.OnnxLayoutAnalysis.UnitTests.csproj", "MEDI/test/PdfPig.DataIngestion.UnitTests/PdfPig.DataIngestion.UnitTests.csproj" diff --git a/MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderReranker.cs b/MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderReranker.cs new file mode 100644 index 0000000..547c5cb --- /dev/null +++ b/MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderReranker.cs @@ -0,0 +1,156 @@ +using CommunityToolkit.DataRetrieval.OnnxReranker.Internal; +using Microsoft.Extensions.DataRetrieval; +using Microsoft.ML.Tokenizers; + +namespace CommunityToolkit.DataRetrieval.OnnxReranker; + +/// +/// ONNX cross-encoder reranker. Scores query-document pairs using a local cross-encoder +/// model (e.g., ms-marco-MiniLM-L-6-v2, BGE-reranker) for fast, high-quality reranking. +/// +/// Pipeline: text-pair tokenization → ONNX inference → sigmoid normalization. +/// +/// Dependencies: Microsoft.ML.OnnxRuntime.Managed + Microsoft.ML.Tokenizers (no Microsoft.ML). +/// +/// +/// Lazy initialization: the ONNX session and tokenizer are loaded on first use. +/// Call to release native ONNX resources. +/// +public class CrossEncoderReranker : RetrievalResultProcessor, IReranker, IDisposable +{ + private readonly CrossEncoderRerankerOptions _options; + private readonly object _initLock = new(); + + private OnnxModelSession? _session; + private TextPairTokenizer? _pairTokenizer; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// Reranker configuration options. + public CrossEncoderReranker(CrossEncoderRerankerOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + + if (string.IsNullOrWhiteSpace(options.ModelPath)) + throw new ArgumentException("ModelPath is required.", nameof(options)); + if (string.IsNullOrWhiteSpace(options.TokenizerPath)) + throw new ArgumentException("TokenizerPath is required.", nameof(options)); + } + + /// + public override async Task ProcessAsync( + RetrievalResults results, RetrievalQuery query, CancellationToken cancellationToken = default) + { + if (results.Chunks.Count <= 1) + return results; + + var reranked = await RerankAsync( + query.Text, + results.Chunks.ToList(), + cancellationToken); + + results.Chunks = reranked.Take(_options.MaxResults).ToList(); + results.Metadata["reranked"] = true; + results.Metadata["reranker"] = "CrossEncoder"; + results.Metadata["reranked_count"] = results.Chunks.Count; + return results; + } + + /// + public Task> RerankAsync( + string query, + IReadOnlyList chunks, + CancellationToken cancellationToken = default) + { + if (chunks.Count == 0) + return Task.FromResult>([]); + + return Task.Run(() => + { + EnsureInitialized(); + cancellationToken.ThrowIfCancellationRequested(); + + var documents = chunks.Select(r => r.Content).ToList(); + var scores = ScoreDocuments(query, documents); + + var reranked = chunks + .Select((chunk, i) => + { + chunk.Score = scores[i]; + return chunk; + }) + .OrderByDescending(c => c.Score) + .ToList(); + + return (IReadOnlyList)reranked; + }, cancellationToken); + } + + private float[] ScoreDocuments(string query, IList documents) + { + var allScores = new List(documents.Count); + int batchSize = _options.BatchSize; + + for (int start = 0; start < documents.Count; start += batchSize) + { + int count = Math.Min(batchSize, documents.Count - start); + var batchQueries = Enumerable.Repeat(query, count).ToList(); + var batchDocs = documents.Skip(start).Take(count).ToList(); + + var tokenized = _pairTokenizer!.TokenizePairs(batchQueries, batchDocs); + var rawOutputs = _session!.Score(tokenized); + + for (int i = 0; i < rawOutputs.Length; i++) + { + float logit = rawOutputs[i][0]; + allScores.Add(Sigmoid(logit)); + } + } + + return [.. allScores]; + } + + private void EnsureInitialized() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (_session != null && _pairTokenizer != null) + return; + + lock (_initLock) + { + if (_session != null && _pairTokenizer != null) + return; + + var tokenizer = TokenizerFactory.LoadTokenizer(_options.TokenizerPath); + var (bosId, sepId, doubleSep) = TokenizerFactory.DiscoverSpecialTokens(_options.TokenizerPath); + + if (bosId == null || sepId == null) + throw new InvalidOperationException( + "Could not determine BOS/CLS and SEP token IDs for text-pair tokenization. " + + "Ensure the tokenizer directory contains a tokenizer_config.json with cls_token/sep_token " + + "or a recognized tokenizer_class."); + + _pairTokenizer = new TextPairTokenizer( + tokenizer, _options.MaxTokenLength, bosId.Value, sepId.Value, doubleSep); + + _session = new OnnxModelSession( + _options.ModelPath, _options.GpuDeviceId, _options.FallbackToCpu, _options.PreferredOutputNames); + } + } + + private static float Sigmoid(float x) => 1.0f / (1.0f + MathF.Exp(-x)); + + /// + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _session?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderRerankerOptions.cs b/MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderRerankerOptions.cs new file mode 100644 index 0000000..cdf38d9 --- /dev/null +++ b/MEDI/src/DataRetrieval.OnnxReranker/CrossEncoderRerankerOptions.cs @@ -0,0 +1,43 @@ +namespace CommunityToolkit.DataRetrieval.OnnxReranker; + +/// +/// Configuration for the ONNX cross-encoder reranker. +/// +public sealed class CrossEncoderRerankerOptions +{ + /// Path to the ONNX cross-encoder model file (.onnx). + public required string ModelPath { get; set; } + + /// + /// Path to tokenizer artifacts — either a directory containing tokenizer_config.json + /// (or vocab.txt, tokenizer.model, etc.) or a direct path to a vocab file. + /// + public required string TokenizerPath { get; set; } + + /// Maximum token length for the combined query + document pair. Default: 512. + public int MaxTokenLength { get; set; } = 512; + + /// Batch size for ONNX inference. Default: 32. + public int BatchSize { get; set; } = 32; + + /// + /// Optional GPU device ID. Null = CPU only. + /// Requires the consuming application to reference Microsoft.ML.OnnxRuntime.Gpu. + /// + public int? GpuDeviceId { get; set; } + + /// + /// If true and GPU initialization fails, fall back to CPU instead of throwing. + /// Default: false. + /// + public bool FallbackToCpu { get; set; } + + /// Maximum number of results to return after reranking. Default: 5. + public int MaxResults { get; set; } = 5; + + /// + /// Preferred ONNX output tensor names, searched in order. + /// Default: ["logits", "output"] — appropriate for cross-encoder models. + /// + public string[] PreferredOutputNames { get; set; } = ["logits", "output"]; +} diff --git a/MEDI/src/DataRetrieval.OnnxReranker/DataRetrieval.OnnxReranker.csproj b/MEDI/src/DataRetrieval.OnnxReranker/DataRetrieval.OnnxReranker.csproj new file mode 100644 index 0000000..75a0021 --- /dev/null +++ b/MEDI/src/DataRetrieval.OnnxReranker/DataRetrieval.OnnxReranker.csproj @@ -0,0 +1,24 @@ + + + 1.0.0-preview.1 + CommunityToolkit.DataRetrieval.OnnxReranker + $(AssemblyName) + net10.0;net8.0 + + ONNX cross-encoder re-ranking for DataRetrieval pipelines + ONNX Runtime cross-encoder re-ranker implementing IReranker for high-quality, low-latency passage re-ranking without LLM API calls. + true + + + + + + + + + + + + + + diff --git a/MEDI/src/DataRetrieval.OnnxReranker/Internal/OnnxModelSession.cs b/MEDI/src/DataRetrieval.OnnxReranker/Internal/OnnxModelSession.cs new file mode 100644 index 0000000..b0c454b --- /dev/null +++ b/MEDI/src/DataRetrieval.OnnxReranker/Internal/OnnxModelSession.cs @@ -0,0 +1,186 @@ +using Microsoft.ML.OnnxRuntime; + +namespace CommunityToolkit.DataRetrieval.OnnxReranker.Internal; + +/// +/// Discovered ONNX model tensor metadata. +/// +internal sealed record OnnxModelMetadata( + string InputIdsName, + string AttentionMaskName, + string? TokenTypeIdsName, + string OutputTensorName, + int HiddenDim, + int OutputRank); + +/// +/// Manages an ONNX InferenceSession for cross-encoder scoring. +/// Handles session creation, metadata discovery, batch inference, and disposal. +/// +internal sealed class OnnxModelSession : IDisposable +{ + private readonly InferenceSession _session; + private readonly OnnxModelMetadata _metadata; + + public OnnxModelMetadata Metadata => _metadata; + + public OnnxModelSession(string modelPath, int? gpuDeviceId, bool fallbackToCpu, string[]? preferredOutputNames) + { + if (!File.Exists(modelPath)) + throw new FileNotFoundException($"ONNX model not found: {modelPath}"); + + _session = CreateSession(modelPath, gpuDeviceId, fallbackToCpu); + _metadata = DiscoverMetadata(_session, preferredOutputNames); + } + + /// + /// Runs ONNX inference on a tokenized batch. Returns raw logits per item. + /// + public float[][] Score(TokenizedBatch batch) + { + int totalRows = batch.Count; + int seqLen = batch.SequenceLength; + var allOutputs = new List(totalRows); + + int batchSize = totalRows; + for (int start = 0; start < totalRows; start += batchSize) + { + int count = Math.Min(batchSize, totalRows - start); + var batchOutputs = RunOnnxBatch( + batch.TokenIds, batch.AttentionMasks, batch.TokenTypeIds, + start, count, seqLen); + allOutputs.AddRange(batchOutputs); + } + + return [.. allOutputs]; + } + + private float[][] RunOnnxBatch( + long[][] tokenIds, long[][] attentionMasks, long[][] tokenTypeIds, + int startIdx, int batchSize, int seqLen) + { + var idsArray = new long[batchSize * seqLen]; + var maskArray = new long[batchSize * seqLen]; + var typeIdsArray = _metadata.TokenTypeIdsName != null ? new long[batchSize * seqLen] : null; + + for (int b = 0; b < batchSize; b++) + { + Array.Copy(tokenIds[startIdx + b], 0, idsArray, b * seqLen, seqLen); + Array.Copy(attentionMasks[startIdx + b], 0, maskArray, b * seqLen, seqLen); + if (typeIdsArray != null) + Array.Copy(tokenTypeIds[startIdx + b], 0, typeIdsArray, b * seqLen, seqLen); + } + + var inputs = new Dictionary + { + [_metadata.InputIdsName] = OrtValue.CreateTensorValueFromMemory(idsArray, [batchSize, seqLen]), + [_metadata.AttentionMaskName] = OrtValue.CreateTensorValueFromMemory(maskArray, [batchSize, seqLen]) + }; + + if (_metadata.TokenTypeIdsName != null && typeIdsArray != null) + inputs[_metadata.TokenTypeIdsName] = OrtValue.CreateTensorValueFromMemory(typeIdsArray, [batchSize, seqLen]); + + try + { + using var results = _session.Run(new RunOptions(), inputs, [_metadata.OutputTensorName]); + var output = results[0]; + var outputSpan = output.GetTensorDataAsSpan(); + + var batchOutputs = new float[batchSize][]; + + if (_metadata.OutputRank == 2) + { + for (int b = 0; b < batchSize; b++) + batchOutputs[b] = outputSpan.Slice(b * _metadata.HiddenDim, _metadata.HiddenDim).ToArray(); + } + else + { + int rowSize = seqLen * _metadata.HiddenDim; + for (int b = 0; b < batchSize; b++) + batchOutputs[b] = outputSpan.Slice(b * rowSize, rowSize).ToArray(); + } + + return batchOutputs; + } + finally + { + foreach (var ortValue in inputs.Values) + ortValue.Dispose(); + } + } + + private static InferenceSession CreateSession(string modelPath, int? gpuDeviceId, bool fallbackToCpu) + { + var options = new SessionOptions(); + + if (gpuDeviceId.HasValue) + { + try + { + options.AppendExecutionProvider_CUDA(gpuDeviceId.Value); + } + catch (Exception) when (fallbackToCpu) + { + // CUDA libraries not available — fall back to CPU + } + } + + try + { + return new InferenceSession(modelPath, options); + } + catch (OnnxRuntimeException) when (fallbackToCpu) + { + return new InferenceSession(modelPath, new SessionOptions()); + } + } + + private static OnnxModelMetadata DiscoverMetadata(InferenceSession session, string[]? preferredOutputNames) + { + var inputMeta = session.InputMetadata; + var outputMeta = session.OutputMetadata; + + string inputIdsName = FindTensorName(inputMeta, ["input_ids"], inputMeta.Keys.First()); + string attentionMaskName = FindTensorName(inputMeta, ["attention_mask"], "attention_mask"); + string? tokenTypeIdsName = TryFindTensorName(inputMeta, ["token_type_ids"]); + + string outputName; + + if (preferredOutputNames != null) + { + var preferred = TryFindTensorName(outputMeta, preferredOutputNames); + outputName = preferred ?? outputMeta.Keys.First(); + } + else + { + var pooledName = TryFindTensorName(outputMeta, ["logits", "output", "sentence_embedding", "pooler_output"]); + outputName = pooledName ?? outputMeta.Keys.First(); + } + + var dims = outputMeta[outputName].Dimensions; + int outputRank = dims.Length; + int lastDim = (int)dims.Last(); + int hiddenDim = lastDim > 0 ? lastDim : 1; + + return new OnnxModelMetadata(inputIdsName, attentionMaskName, tokenTypeIdsName, outputName, hiddenDim, outputRank); + } + + private static string FindTensorName( + IReadOnlyDictionary metadata, string[] candidates, string fallback) + { + return TryFindTensorName(metadata, candidates) ?? fallback; + } + + private static string? TryFindTensorName( + IReadOnlyDictionary metadata, string[] candidates) + { + foreach (var candidate in candidates) + { + if (metadata.ContainsKey(candidate)) + return candidate; + } + return null; + } + + public void Dispose() => _session.Dispose(); +} diff --git a/MEDI/src/DataRetrieval.OnnxReranker/Internal/TextPairTokenizer.cs b/MEDI/src/DataRetrieval.OnnxReranker/Internal/TextPairTokenizer.cs new file mode 100644 index 0000000..75908e0 --- /dev/null +++ b/MEDI/src/DataRetrieval.OnnxReranker/Internal/TextPairTokenizer.cs @@ -0,0 +1,110 @@ +using Microsoft.ML.Tokenizers; + +namespace CommunityToolkit.DataRetrieval.OnnxReranker.Internal; + +/// +/// Batch of tokenized text pairs for ONNX cross-encoder inference. +/// +internal sealed class TokenizedBatch +{ + public long[][] TokenIds { get; } + public long[][] AttentionMasks { get; } + public long[][] TokenTypeIds { get; } + public int SequenceLength { get; } + public int Count => TokenIds.Length; + + public TokenizedBatch(long[][] tokenIds, long[][] attentionMasks, long[][] tokenTypeIds, int sequenceLength) + { + TokenIds = tokenIds; + AttentionMasks = attentionMasks; + TokenTypeIds = tokenTypeIds; + SequenceLength = sequenceLength; + } +} + +/// +/// Tokenizes query-document pairs for cross-encoder models. +/// Produces [CLS] query_tokens [SEP] document_tokens [SEP] with proper token_type_ids. +/// +internal sealed class TextPairTokenizer +{ + private readonly Tokenizer _tokenizer; + private readonly int _maxTokenLength; + private readonly int _bosTokenId; + private readonly int _sepTokenId; + private readonly bool _doubleSeparator; + + public TextPairTokenizer(Tokenizer tokenizer, int maxTokenLength, int bosTokenId, int sepTokenId, bool doubleSeparator = false) + { + _tokenizer = tokenizer ?? throw new ArgumentNullException(nameof(tokenizer)); + _maxTokenLength = maxTokenLength; + _bosTokenId = bosTokenId; + _sepTokenId = sepTokenId; + _doubleSeparator = doubleSeparator; + } + + /// + /// Tokenizes a batch of query-document pairs for cross-encoder scoring. + /// + public TokenizedBatch TokenizePairs(IReadOnlyList queries, IReadOnlyList documents) + { + if (queries.Count != documents.Count) + throw new ArgumentException("queries and documents must have the same length."); + + int batchSize = queries.Count; + var allTokenIds = new long[batchSize][]; + var allAttentionMasks = new long[batchSize][]; + var allTokenTypeIds = new long[batchSize][]; + + for (int i = 0; i < batchSize; i++) + { + var tokenIds = new long[_maxTokenLength]; + var attentionMask = new long[_maxTokenLength]; + var tokenTypeIds = new long[_maxTokenLength]; + + TokenizeSinglePair(queries[i], documents[i], tokenIds, attentionMask, tokenTypeIds); + + allTokenIds[i] = tokenIds; + allAttentionMasks[i] = attentionMask; + allTokenTypeIds[i] = tokenTypeIds; + } + + return new TokenizedBatch(allTokenIds, allAttentionMasks, allTokenTypeIds, _maxTokenLength); + } + + private void TokenizeSinglePair( + string textA, string textB, + long[] tokenIds, long[] attentionMask, long[] tokenTypeIds) + { + var encodedA = _tokenizer.EncodeToTokens(textA, out _); + var encodedB = _tokenizer.EncodeToTokens(textB, out _); + + var combined = new List(_maxTokenLength); + combined.Add(_bosTokenId); + + for (int j = 0; j < encodedA.Count; j++) + combined.Add(encodedA[j].Id); + + combined.Add(_sepTokenId); + + if (_doubleSeparator) + combined.Add(_sepTokenId); + + int firstSepIdx = combined.Count - 1; + + for (int j = 0; j < encodedB.Count; j++) + combined.Add(encodedB[j].Id); + + combined.Add(_sepTokenId); + + if (combined.Count > _maxTokenLength) + combined.RemoveRange(_maxTokenLength, combined.Count - _maxTokenLength); + + for (int s = 0; s < combined.Count; s++) + { + tokenIds[s] = combined[s]; + attentionMask[s] = 1; + tokenTypeIds[s] = s <= firstSepIdx ? 0 : 1; + } + } +} diff --git a/MEDI/src/DataRetrieval.OnnxReranker/Internal/TokenizerFactory.cs b/MEDI/src/DataRetrieval.OnnxReranker/Internal/TokenizerFactory.cs new file mode 100644 index 0000000..3682ab8 --- /dev/null +++ b/MEDI/src/DataRetrieval.OnnxReranker/Internal/TokenizerFactory.cs @@ -0,0 +1,248 @@ +using System.Text.Json; +using Microsoft.ML.Tokenizers; + +namespace CommunityToolkit.DataRetrieval.OnnxReranker.Internal; + +/// +/// Loads a HuggingFace-compatible tokenizer from a directory or file path. +/// Supports BERT/WordPiece, SentencePiece, and BPE tokenizer formats. +/// +internal static class TokenizerFactory +{ + /// + /// Loads a tokenizer from the given path. Accepts a directory (with tokenizer_config.json + /// or known vocab files) or a direct path to a config/vocab file. + /// + public static Tokenizer LoadTokenizer(string path) + { + if (Directory.Exists(path)) + return LoadFromDirectory(path); + + var fileName = Path.GetFileName(path).ToLowerInvariant(); + if (fileName == "tokenizer_config.json") + return LoadFromConfig(path); + + if (fileName == "tokenizer.json") + return LoadFromHuggingFaceTokenizerJson(path); + + return LoadFromVocabFile(path); + } + + /// + /// Extracts BOS (CLS) and SEP token IDs from tokenizer_config.json. + /// Also determines whether the model uses double separators (RoBERTa-family). + /// + public static (int? BosTokenId, int? SepTokenId, bool DoubleSeparator) DiscoverSpecialTokens(string tokenizerPath) + { + string? configPath = null; + + if (Directory.Exists(tokenizerPath)) + { + var candidate = Path.Combine(tokenizerPath, "tokenizer_config.json"); + if (File.Exists(candidate)) + configPath = candidate; + } + else if (Path.GetFileName(tokenizerPath).Equals("tokenizer_config.json", StringComparison.OrdinalIgnoreCase)) + { + configPath = tokenizerPath; + } + + if (configPath == null) + return (null, null, false); + + var json = File.ReadAllText(configPath); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + int? bosId = null; + int? sepId = null; + + var clsTokenStr = root.TryGetProperty("cls_token", out var cls) ? cls.GetString() : null; + var sepTokenStr = root.TryGetProperty("sep_token", out var sep) ? sep.GetString() : null; + + if (clsTokenStr != null && sepTokenStr != null + && root.TryGetProperty("added_tokens_decoder", out var decoder)) + { + foreach (var entry in decoder.EnumerateObject()) + { + if (!int.TryParse(entry.Name, out int tokenId)) continue; + var content = entry.Value.TryGetProperty("content", out var c) ? c.GetString() : null; + if (content == null) continue; + + if (content == clsTokenStr && bosId == null) + bosId = tokenId; + if (content == sepTokenStr && sepId == null) + sepId = tokenId; + } + } + + if (bosId == null || sepId == null) + { + var tokenizerClass = root.TryGetProperty("tokenizer_class", out var tc) ? tc.GetString() ?? "" : ""; + if (tokenizerClass.EndsWith("Fast", StringComparison.Ordinal)) + tokenizerClass = tokenizerClass[..^4]; + + (int? defaultBos, int? defaultSep) = tokenizerClass switch + { + "BertTokenizer" or "DistilBertTokenizer" => ((int?)101, (int?)102), + "RobertaTokenizer" or "GPT2Tokenizer" => ((int?)0, (int?)2), + "DebertaTokenizer" or "DebertaV2Tokenizer" => ((int?)1, (int?)2), + "XLMRobertaTokenizer" => ((int?)0, (int?)2), + _ => ((int?)null, (int?)null) + }; + bosId ??= defaultBos; + sepId ??= defaultSep; + } + + var tokClass = root.TryGetProperty("tokenizer_class", out var t) ? t.GetString() ?? "" : ""; + if (tokClass.EndsWith("Fast", StringComparison.Ordinal)) + tokClass = tokClass[..^4]; + bool doubleSep = tokClass is "RobertaTokenizer" or "GPT2Tokenizer" or "XLMRobertaTokenizer"; + + return (bosId, sepId, doubleSep); + } + + private static Tokenizer LoadFromDirectory(string directory) + { + var configPath = Path.Combine(directory, "tokenizer_config.json"); + if (File.Exists(configPath)) + return LoadFromConfig(configPath); + + var vocabTxt = Path.Combine(directory, "vocab.txt"); + if (File.Exists(vocabTxt)) + return LoadFromVocabFile(vocabTxt); + + var spModel = Path.Combine(directory, "tokenizer.model"); + if (File.Exists(spModel)) + return LoadFromVocabFile(spModel); + + var spBpeModel = Path.Combine(directory, "sentencepiece.bpe.model"); + if (File.Exists(spBpeModel)) + return LoadFromVocabFile(spBpeModel); + + var spmModel = Path.Combine(directory, "spm.model"); + if (File.Exists(spmModel)) + return LoadFromVocabFile(spmModel); + + var tokenizerJson = Path.Combine(directory, "tokenizer.json"); + if (File.Exists(tokenizerJson)) + return LoadFromHuggingFaceTokenizerJson(tokenizerJson); + + throw new FileNotFoundException( + $"No tokenizer_config.json or known vocab file found in '{directory}'. " + + $"Expected one of: tokenizer_config.json, vocab.txt, tokenizer.model, sentencepiece.bpe.model, spm.model, tokenizer.json."); + } + + private static Tokenizer LoadFromConfig(string configPath) + { + var directory = Path.GetDirectoryName(configPath) + ?? throw new ArgumentException($"Cannot determine directory for config: {configPath}"); + + var json = File.ReadAllText(configPath); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + var tokenizerClass = root.TryGetProperty("tokenizer_class", out var tcls) + ? tcls.GetString() ?? "" + : ""; + + if (tokenizerClass.EndsWith("Fast", StringComparison.Ordinal)) + tokenizerClass = tokenizerClass[..^4]; + + return tokenizerClass switch + { + "BertTokenizer" => LoadBertFromConfig(directory, root), + "DistilBertTokenizer" => LoadBertFromConfig(directory, root), + "XLMRobertaTokenizer" => LoadSentencePieceFromDirectory(directory), + "LlamaTokenizer" => LoadSentencePieceFromDirectory(directory), + "CamembertTokenizer" => LoadSentencePieceFromDirectory(directory), + "T5Tokenizer" => LoadSentencePieceFromDirectory(directory), + "AlbertTokenizer" => LoadSentencePieceFromDirectory(directory), + "DebertaTokenizer" => LoadSentencePieceFromDirectory(directory), + "DebertaV2Tokenizer" => LoadSentencePieceFromDirectory(directory), + "GPT2Tokenizer" => LoadBpeFromDirectory(directory), + "RobertaTokenizer" => LoadBpeFromDirectory(directory), + _ when !string.IsNullOrEmpty(tokenizerClass) => throw new NotSupportedException( + $"Unsupported tokenizer_class '{tokenizerClass}' in {configPath}. " + + $"Supported: BertTokenizer, DebertaV2Tokenizer, XLMRobertaTokenizer, LlamaTokenizer, GPT2Tokenizer, RobertaTokenizer. " + + $"Use a tokenizer_config.json with a supported tokenizer_class."), + _ => throw new InvalidOperationException( + $"No tokenizer_class found in {configPath}. Cannot auto-detect tokenizer type.") + }; + } + + private static Tokenizer LoadBertFromConfig(string directory, JsonElement config) + { + var vocabPath = Path.Combine(directory, "vocab.txt"); + if (!File.Exists(vocabPath)) + throw new FileNotFoundException( + $"BERT tokenizer requires vocab.txt in '{directory}'."); + + var lowerCase = config.TryGetProperty("do_lower_case", out var lc) && lc.GetBoolean(); + + using var stream = File.OpenRead(vocabPath); + return BertTokenizer.Create(stream, new BertOptions { LowerCaseBeforeTokenization = lowerCase }); + } + + private static Tokenizer LoadSentencePieceFromDirectory(string directory) + { + var candidates = new[] { "sentencepiece.bpe.model", "tokenizer.model", "spiece.model", "spm.model" }; + foreach (var candidate in candidates) + { + var spPath = Path.Combine(directory, candidate); + if (File.Exists(spPath)) + { + using var stream = File.OpenRead(spPath); + return SentencePieceTokenizer.Create(stream); + } + } + + throw new FileNotFoundException( + $"SentencePiece tokenizer requires one of [{string.Join(", ", candidates)}] in '{directory}'."); + } + + private static Tokenizer LoadBpeFromDirectory(string directory) + { + var vocabJson = Path.Combine(directory, "vocab.json"); + var mergesPath = Path.Combine(directory, "merges.txt"); + + if (!File.Exists(vocabJson)) + throw new FileNotFoundException($"BPE tokenizer requires vocab.json in '{directory}'."); + if (!File.Exists(mergesPath)) + throw new FileNotFoundException($"BPE tokenizer requires merges.txt in '{directory}'."); + + using var vocabStream = File.OpenRead(vocabJson); + using var mergesStream = File.OpenRead(mergesPath); + return CodeGenTokenizer.Create(vocabStream, mergesStream); + } + + private static Tokenizer LoadFromHuggingFaceTokenizerJson(string path) + { + var directory = Path.GetDirectoryName(path); + if (directory != null) + { + var vocabTxt = Path.Combine(directory, "vocab.txt"); + if (File.Exists(vocabTxt)) + return LoadFromVocabFile(vocabTxt); + } + + throw new NotSupportedException( + $"tokenizer.json without a tokenizer_config.json is not supported for cross-encoder reranking. " + + $"Please provide a directory with tokenizer_config.json and vocab.txt."); + } + + private static Tokenizer LoadFromVocabFile(string path) + { + var ext = Path.GetExtension(path).ToLowerInvariant(); + using var stream = File.OpenRead(path); + return ext switch + { + ".txt" => BertTokenizer.Create(stream), + ".model" => SentencePieceTokenizer.Create(stream), + _ => throw new NotSupportedException( + $"Unsupported vocab file extension '{ext}'. " + + $"Use .txt for BERT/WordPiece, .model for SentencePiece, " + + $"or point to a directory with tokenizer_config.json.") + }; + } +} diff --git a/MEDI/src/DataRetrieval/AdaptiveRouter.cs b/MEDI/src/DataRetrieval/AdaptiveRouter.cs new file mode 100644 index 0000000..376dfcc --- /dev/null +++ b/MEDI/src/DataRetrieval/AdaptiveRouter.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Adaptive RAG router — classifies queries and routes to the appropriate search paradigm. +/// +/// +/// Routes between: +/// +/// Vector: specific factual queries → standard vector search +/// TreeTraversal: broad/thematic queries → top-down tree traversal +/// EntityFiltered: entity-specific queries → vector search with entity metadata filter +/// +/// +public class AdaptiveRouter : RetrievalQueryProcessor +{ + private readonly IChatClient _chatClient; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client for LLM inference. + public AdaptiveRouter(IChatClient chatClient) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + RetrievalQuery query, CancellationToken cancellationToken = default) + { + var prompt = $$""" + Classify this search query into one of these categories: + - "vector": specific factual question about a particular topic, API, or concept + - "tree": broad/thematic question requiring multi-document overview or summary + - "entity": question about a specific named entity (person, organization, technology) + + Query: "{{query.Text}}" + + Return ONLY valid JSON: {"paradigm": "vector", "reasoning": "one sentence"} + """; + + string paradigm; + try + { + var response = await _chatClient.GetResponseAsync(prompt, + new ChatOptions + { + MaxOutputTokens = 100, + ResponseFormat = ChatResponseFormat.Json + }, cancellationToken); + + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + + paradigm = result?.Paradigm switch + { + "vector" or "tree" or "entity" => result.Paradigm, + _ => "vector" + }; + + if (result?.Reasoning is not null) + query.Metadata["router_reasoning"] = result.Reasoning; + } + catch + { + paradigm = "vector"; + } + + query.Metadata["search_paradigm"] = paradigm; + return query; + } + + private sealed class RouterResponse + { + [JsonPropertyName("paradigm")] + public string Paradigm { get; set; } = "vector"; + + [JsonPropertyName("reasoning")] + public string? Reasoning { get; set; } + } +} diff --git a/MEDI/src/DataRetrieval/CragValidator.cs b/MEDI/src/DataRetrieval/CragValidator.cs new file mode 100644 index 0000000..60e5d50 --- /dev/null +++ b/MEDI/src/DataRetrieval/CragValidator.cs @@ -0,0 +1,119 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Corrective RAG (CRAG) validator — scores retrieval quality and routes through +/// one of three paths: +/// +/// Correct (≥4): use results as-is +/// Ambiguous (2-3): flag for follow-up, keep results with warning +/// Incorrect (<2): clear chunks, set low_confidence flag +/// +/// +public class CragValidator : RetrievalResultProcessor +{ + private readonly IChatClient _chatClient; + + /// Number of top chunks to evaluate. + public int EvaluateTopN { get; init; } = 3; + + /// Maximum preview length per passage (characters). + public int PreviewLength { get; init; } = 300; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client for LLM inference. + public CragValidator(IChatClient chatClient) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + RetrievalResults results, RetrievalQuery query, CancellationToken cancellationToken = default) + { + if (results.Chunks.Count == 0) + { + results.Metadata["crag_score"] = 0; + results.Metadata["crag_path"] = "Incorrect"; + results.Metadata["low_confidence"] = true; + return results; + } + + var topChunks = results.Chunks.Take(EvaluateTopN).ToList(); + var passagesText = string.Join("\n---\n", + topChunks.Select(c => c.Content.Length > PreviewLength + ? c.Content[..PreviewLength] + : c.Content)); + + var prompt = $$""" + Rate how well these passages answer the query on a scale of 1-5: + 5 = directly answers with specific details + 4 = highly relevant, mostly answers + 3 = somewhat related but incomplete + 2 = tangentially related + 1 = completely different topic + + Query: {{query.Text}} + Passages: + {{passagesText}} + + Return ONLY valid JSON: {"score": 5, "reasoning": "explanation"} + """; + + var options = new ChatOptions + { + MaxOutputTokens = 200, + ResponseFormat = ChatResponseFormat.Json + }; + + int score; + try + { + var response = await _chatClient.GetResponseAsync(prompt, options, cancellationToken); + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + score = result?.Score is >= 1 and <= 5 ? result.Score : 3; + if (result?.Reasoning is not null) + results.Metadata["crag_reasoning"] = result.Reasoning; + } + catch + { + score = 3; + } + + results.Metadata["crag_score"] = score; + + if (score >= 4) + { + results.Metadata["crag_path"] = "Correct"; + } + else if (score >= 2) + { + results.Metadata["crag_path"] = "Ambiguous"; + results.Metadata["needs_followup"] = true; + } + else + { + results.Metadata["crag_path"] = "Incorrect"; + results.Metadata["low_confidence"] = true; + results.Chunks = []; + } + + return results; + } + + private sealed class CragResponse + { + [JsonPropertyName("score")] + public int Score { get; set; } + + [JsonPropertyName("reasoning")] + public string? Reasoning { get; set; } + } +} diff --git a/MEDI/src/DataRetrieval/DataRetrieval.csproj b/MEDI/src/DataRetrieval/DataRetrieval.csproj new file mode 100644 index 0000000..fdcbab0 --- /dev/null +++ b/MEDI/src/DataRetrieval/DataRetrieval.csproj @@ -0,0 +1,26 @@ + + + 1.0.0-preview.1 + CommunityToolkit.DataRetrieval + $(AssemblyName) + net10.0;net8.0 + + Advanced retrieval processors for Microsoft.Extensions.DataRetrieval pipelines + LLM-powered retrieval pipeline processors including multi-query expansion, HyDE, adaptive routing, CRAG validation, LLM re-ranking, Self-RAG, and Speculative RAG for advanced RAG applications. + true + + + + + + + + + + + + + + + + diff --git a/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs b/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs new file mode 100644 index 0000000..16e07d2 --- /dev/null +++ b/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.DataRetrieval; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RetrievalPipeline = Microsoft.Extensions.DataRetrieval.RetrievalPipeline; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Extension methods for registering retrieval pipeline builders with dependency injection. +/// +public static class DataRetrievalServiceCollectionExtensions +{ + /// + /// Registers a singleton and returns a + /// for composing processors. + /// + /// + /// + /// builder.Services.AddRetrievalPipeline() + /// .UseQueryExpansion(o => o.VariantCount = 5) + /// .UseLlmReranking() + /// .UseCrag(); + /// + /// + public static RetrievalPipelineBuilder AddRetrievalPipeline( + this IServiceCollection services) + { + var pipelineBuilder = new RetrievalPipelineBuilder(); + + services.AddSingleton(sp => + { + var loggerFactory = sp.GetService(); + var pipeline = new RetrievalPipeline(loggerFactory: loggerFactory); + + foreach (var factory in pipelineBuilder.QueryProcessorFactories) + pipeline.QueryProcessors.Add(factory(sp)); + + foreach (var factory in pipelineBuilder.ResultProcessorFactories) + pipeline.ResultProcessors.Add(factory(sp)); + + return pipeline; + }); + + return pipelineBuilder; + } + + /// + /// Registers a singleton using a custom factory and returns a + /// for composing processors on top. + /// + public static RetrievalPipelineBuilder AddRetrievalPipeline( + this IServiceCollection services, + Func pipelineFactory) + { + var pipelineBuilder = new RetrievalPipelineBuilder(); + + services.AddSingleton(sp => + { + var pipeline = pipelineFactory(sp); + + foreach (var factory in pipelineBuilder.QueryProcessorFactories) + pipeline.QueryProcessors.Add(factory(sp)); + + foreach (var factory in pipelineBuilder.ResultProcessorFactories) + pipeline.ResultProcessors.Add(factory(sp)); + + return pipeline; + }); + + return pipelineBuilder; + } +} diff --git a/MEDI/src/DataRetrieval/HydeQueryTransformer.cs b/MEDI/src/DataRetrieval/HydeQueryTransformer.cs new file mode 100644 index 0000000..4b0829e --- /dev/null +++ b/MEDI/src/DataRetrieval/HydeQueryTransformer.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Hypothetical Document Embedding (HyDE) — generates a hypothetical answer to the query, +/// then searches using that answer's embedding instead of the raw query. +/// This bridges the query-document semantic gap. +/// +public class HydeQueryTransformer : RetrievalQueryProcessor +{ + private readonly IChatClient _chatClient; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client for LLM inference. + public HydeQueryTransformer(IChatClient chatClient) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + RetrievalQuery query, CancellationToken cancellationToken = default) + { + var prompt = $""" + Write a short, factual paragraph (3-4 sentences) that would directly answer this question. + Write as if you are a documentation page. Do NOT include any preamble or explanation. + + Question: {query.Text} + """; + + var options = new ChatOptions + { + MaxOutputTokens = 250 + }; + + string hypothetical; + try + { + var response = await _chatClient.GetResponseAsync(prompt, options, cancellationToken); + hypothetical = (response.Text ?? "").Trim(); + } + catch + { + return query; + } + + if (string.IsNullOrWhiteSpace(hypothetical)) + return query; + + query.Variants = [hypothetical]; + query.Metadata["hyde_hypothetical"] = hypothetical; + return query; + } +} diff --git a/MEDI/src/DataRetrieval/JsonDefaults.cs b/MEDI/src/DataRetrieval/JsonDefaults.cs new file mode 100644 index 0000000..9402378 --- /dev/null +++ b/MEDI/src/DataRetrieval/JsonDefaults.cs @@ -0,0 +1,13 @@ +using System.Text.Json; + +namespace CommunityToolkit.DataRetrieval; + +/// Shared JSON serialization defaults for LLM response parsing. +internal static class JsonDefaults +{ + public static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; +} diff --git a/MEDI/src/DataRetrieval/LlmReranker.cs b/MEDI/src/DataRetrieval/LlmReranker.cs new file mode 100644 index 0000000..33febf3 --- /dev/null +++ b/MEDI/src/DataRetrieval/LlmReranker.cs @@ -0,0 +1,108 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// LLM-based reranker — asks the model to rank passages by relevance to the query. +/// Implements for pipeline integration. +/// +public class LlmReranker : RetrievalResultProcessor +{ + private readonly IChatClient _chatClient; + + /// Maximum number of results to return after reranking. + public int MaxResults { get; init; } = 5; + + /// Maximum candidate passages to send to the LLM (controls token cost). + public int MaxCandidates { get; init; } = 8; + + /// Maximum preview length per passage (characters). + public int PreviewLength { get; init; } = 200; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client for LLM inference. + public LlmReranker(IChatClient chatClient) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + RetrievalResults results, RetrievalQuery query, CancellationToken cancellationToken = default) + { + if (results.Chunks.Count <= 2) + return results; + + var candidates = results.Chunks.Take(MaxCandidates).ToList(); + var rankedIndices = await GetRankedIndicesAsync( + query.Text, candidates, cancellationToken); + + var reranked = rankedIndices + .Where(idx => idx >= 1 && idx <= candidates.Count) + .Distinct() + .Take(MaxResults) + .Select(idx => candidates[idx - 1]) + .ToList(); + + if (reranked.Count == 0) + reranked = candidates.Take(MaxResults).ToList(); + + results.Chunks = reranked; + results.Metadata["reranked"] = true; + results.Metadata["reranked_count"] = reranked.Count; + return results; + } + + private async Task GetRankedIndicesAsync( + string query, IList candidates, CancellationToken ct) + { + var passagesBlock = string.Join("\n", + candidates.Select((c, i) => + { + var preview = c.Content.Length > PreviewLength + ? c.Content[..PreviewLength] + "..." + : c.Content; + return $"[{i + 1}] {preview}"; + })); + + var prompt = $$""" + Rank these passages by relevance to the query. Return the passage numbers + in order from most to least relevant. + + Query: {{query}} + Passages: + {{passagesBlock}} + + Return ONLY valid JSON: {"rankedIndices": [3, 1, 5, 2, 4]} + """; + + var options = new ChatOptions + { + MaxOutputTokens = 100, + ResponseFormat = ChatResponseFormat.Json + }; + + try + { + var response = await _chatClient.GetResponseAsync(prompt, options, ct); + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + return result?.RankedIndices ?? []; + } + catch + { + return []; + } + } + + private sealed class RerankingResponse + { + [JsonPropertyName("rankedIndices")] + public int[] RankedIndices { get; set; } = []; + } +} diff --git a/MEDI/src/DataRetrieval/MultiQueryExpander.cs b/MEDI/src/DataRetrieval/MultiQueryExpander.cs new file mode 100644 index 0000000..124db6f --- /dev/null +++ b/MEDI/src/DataRetrieval/MultiQueryExpander.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Generates multiple query variants via LLM to improve recall. +/// The pipeline's RRF (Reciprocal Rank Fusion) merge deduplicates results from all variants. +/// +public class MultiQueryExpander : RetrievalQueryProcessor +{ + private readonly IChatClient _chatClient; + + /// Number of additional query variants to generate. + public int VariantCount { get; init; } = 3; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client for LLM inference. + public MultiQueryExpander(IChatClient chatClient) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async Task ProcessAsync( + RetrievalQuery query, CancellationToken cancellationToken = default) + { + var prompt = $$""" + Given this search query, generate exactly {{VariantCount}} alternative phrasings that capture + different angles or terminology. + + Query: {{query.Text}} + + Return ONLY valid JSON: {"variants": ["query1", "query2", "query3"]} + """; + + var options = new ChatOptions + { + MaxOutputTokens = 200, + ResponseFormat = ChatResponseFormat.Json + }; + + List variants; + try + { + var response = await _chatClient.GetResponseAsync(prompt, options, cancellationToken); + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + + variants = (result?.Variants ?? []) + .Where(v => !string.IsNullOrWhiteSpace(v) && v.Length > 10) + .Take(VariantCount) + .ToList(); + } + catch + { + variants = []; + } + + var allVariants = new List { query.Text }; + allVariants.AddRange(variants); + query.Variants = allVariants; + return query; + } + + private sealed class QueryExpansionResponse + { + [JsonPropertyName("variants")] + public string[] Variants { get; set; } = []; + } +} diff --git a/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs b/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs new file mode 100644 index 0000000..7c876b8 --- /dev/null +++ b/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs @@ -0,0 +1,145 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataRetrieval; +using Microsoft.Extensions.DependencyInjection; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Fluent builder for composing a with query and result processors. +/// Returned by . +/// +public class RetrievalPipelineBuilder +{ + internal List> QueryProcessorFactories { get; } = []; + internal List> ResultProcessorFactories { get; } = []; + + /// Adds a query processor resolved from DI via . + public RetrievalPipelineBuilder UseQueryProcessor() where T : RetrievalQueryProcessor + { + QueryProcessorFactories.Add(sp => ActivatorUtilities.CreateInstance(sp)); + return this; + } + + /// Adds adaptive query routing that classifies queries and selects the best search paradigm. + public RetrievalPipelineBuilder UseAdaptiveRouting() + { + QueryProcessorFactories.Add(sp => + new AdaptiveRouter(sp.GetRequiredService())); + return this; + } + + /// Adds multi-query expansion with Reciprocal Rank Fusion. + public RetrievalPipelineBuilder UseQueryExpansion(Action? configure = null) + { + QueryProcessorFactories.Add(sp => + { + var options = new QueryExpansionOptions(); + configure?.Invoke(options); + return new MultiQueryExpander(sp.GetRequiredService()) + { + VariantCount = options.VariantCount + }; + }); + return this; + } + + /// Adds HyDE (Hypothetical Document Embeddings) query transformation. + public RetrievalPipelineBuilder UseHyDE() + { + QueryProcessorFactories.Add(sp => + new HydeQueryTransformer(sp.GetRequiredService())); + return this; + } + + /// Adds RAPTOR-style tree traversal for hierarchical search. + public RetrievalPipelineBuilder UseTreeSearch(Action? configure = null) + { + QueryProcessorFactories.Add(_ => + { + var options = new TreeSearchOptions(); + configure?.Invoke(options); + return new TreeSearchRetriever + { + ResultsPerLevel = options.ResultsPerLevel + }; + }); + return this; + } + + /// Adds a result processor resolved from DI via . + public RetrievalPipelineBuilder UseResultProcessor() where T : RetrievalResultProcessor + { + ResultProcessorFactories.Add(sp => ActivatorUtilities.CreateInstance(sp)); + return this; + } + + /// Adds LLM-based reranking of search results. + public RetrievalPipelineBuilder UseLlmReranking(Action? configure = null) + { + ResultProcessorFactories.Add(sp => + { + var options = new LlmRerankingOptions(); + configure?.Invoke(options); + return new LlmReranker(sp.GetRequiredService()) + { + MaxResults = options.MaxResults, + MaxCandidates = options.MaxCandidates, + PreviewLength = options.PreviewLength + }; + }); + return this; + } + + /// Adds CRAG quality gate that routes results based on relevance confidence. + public RetrievalPipelineBuilder UseCrag(Action? configure = null) + { + ResultProcessorFactories.Add(sp => + { + var options = new CragOptions(); + configure?.Invoke(options); + return new CragValidator(sp.GetRequiredService()) + { + EvaluateTopN = options.EvaluateTopN, + PreviewLength = options.PreviewLength + }; + }); + return this; + } +} + +/// Options for multi-query expansion. +public class QueryExpansionOptions +{ + /// Number of alternative query variants to generate. + public int VariantCount { get; set; } = 3; +} + +/// Options for tree search retrieval. +public class TreeSearchOptions +{ + /// Number of results per tree level. + public int ResultsPerLevel { get; set; } = 3; +} + +/// Options for LLM-based reranking. +public class LlmRerankingOptions +{ + /// Maximum results to return. + public int MaxResults { get; set; } = 5; + + /// Maximum candidates to evaluate. + public int MaxCandidates { get; set; } = 8; + + /// Maximum preview length per passage. + public int PreviewLength { get; set; } = 200; +} + +/// Options for CRAG validation. +public class CragOptions +{ + /// Number of top chunks to evaluate. + public int EvaluateTopN { get; set; } = 3; + + /// Maximum preview length per passage. + public int PreviewLength { get; set; } = 300; +} diff --git a/MEDI/src/DataRetrieval/SelfRagOrchestrator.cs b/MEDI/src/DataRetrieval/SelfRagOrchestrator.cs new file mode 100644 index 0000000..687e2d5 --- /dev/null +++ b/MEDI/src/DataRetrieval/SelfRagOrchestrator.cs @@ -0,0 +1,231 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.VectorData; +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Self-RAG orchestrator — adaptive retrieval with self-evaluation. +/// Flow: decide retrieval → generate → self-critique → accept or retry with forced retrieval. +/// +/// +/// For compliance, medical, or legal domains where answer quality matters more than speed. +/// +public class SelfRagOrchestrator +{ + private readonly IChatClient _chatClient; + private readonly ILogger? _logger; + + /// Minimum average score (relevance + faithfulness) to accept an answer. + public double AcceptanceThreshold { get; init; } = 3.0; + + /// Maximum retry attempts before accepting best-effort. + public int MaxRetries { get; init; } = 1; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client for LLM inference. + /// Optional logger factory. + public SelfRagOrchestrator(IChatClient chatClient, ILoggerFactory? loggerFactory = null) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + _logger = loggerFactory?.CreateLogger(); + } + + /// + /// Runs the Self-RAG flow for a query against a vector store collection. + /// + public async Task GenerateAsync( + VectorStoreCollection collection, + string query, + Func contentSelector, + int topK = 3, + CancellationToken cancellationToken = default) + where TKey : notnull + where TRecord : class + { + var (needsRetrieval, reasoning) = await DecideRetrievalAsync(query, cancellationToken); + _logger?.LogDebug("Self-RAG retrieval decision: {Decision} — {Reasoning}", needsRetrieval, reasoning); + + List? retrievedChunks = null; + + if (needsRetrieval) + { + retrievedChunks = await RetrieveChunksAsync(collection, query, contentSelector, topK, cancellationToken); + } + + var answer = await GenerateAnswerAsync(query, retrievedChunks, cancellationToken); + + var (relevance, faithfulness, critique) = await SelfCritiqueAsync( + query, answer, retrievedChunks, cancellationToken); + var avgScore = (relevance + faithfulness) / 2.0; + + _logger?.LogDebug("Self-RAG critique: relevance={Relevance}, faithfulness={Faithfulness}, avg={Avg}", + relevance, faithfulness, avgScore); + + if (avgScore < AcceptanceThreshold && MaxRetries > 0) + { + _logger?.LogInformation("Self-RAG score {Score} below threshold {Threshold} — retrying with forced retrieval", + avgScore, AcceptanceThreshold); + + retrievedChunks = await RetrieveChunksAsync(collection, query, contentSelector, topK, cancellationToken); + answer = await GenerateAnswerAsync(query, retrievedChunks, cancellationToken); + (relevance, faithfulness, critique) = await SelfCritiqueAsync( + query, answer, retrievedChunks, cancellationToken); + avgScore = (relevance + faithfulness) / 2.0; + } + + return new SelfRagResult + { + Answer = answer, + NeedsRetrieval = needsRetrieval, + RetrievalReasoning = reasoning, + Relevance = relevance, + Faithfulness = faithfulness, + AverageScore = avgScore, + Critique = critique, + RetrievedChunks = retrievedChunks ?? [] + }; + } + + private async Task<(bool NeedsRetrieval, string Reasoning)> DecideRetrievalAsync( + string query, CancellationToken ct) + { + var prompt = "You are an AI assistant deciding whether external retrieval is needed to answer a query.\n\n" + + "Rules:\n" + + "- If the query asks about SPECIFIC technical details, APIs, or framework features → needsRetrieval = true\n" + + "- If the query asks about GENERAL concepts that are widely known → needsRetrieval = false\n" + + "- If borderline → needsRetrieval = true\n\n" + + $"Query: \"{query}\"\n\n" + + "Return ONLY valid JSON: {\"needsRetrieval\": true, \"reasoning\": \"one sentence\"}"; + + try + { + var response = await _chatClient.GetResponseAsync(prompt, + new ChatOptions { MaxOutputTokens = 200, ResponseFormat = ChatResponseFormat.Json }, ct); + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + return (result?.NeedsRetrieval ?? true, result?.Reasoning ?? ""); + } + catch + { + return (true, "defaulting to retrieval on failure"); + } + } + + private static async Task> RetrieveChunksAsync( + VectorStoreCollection collection, + string query, Func contentSelector, + int topK, CancellationToken ct) + where TKey : notnull + where TRecord : class + { + var results = new List(); + await foreach (var hit in collection.SearchAsync(query, top: topK, cancellationToken: ct)) + { + if (hit.Record is not null) + results.Add(contentSelector(hit.Record)); + } + return results; + } + + private async Task GenerateAnswerAsync( + string query, List? chunks, CancellationToken ct) + { + string prompt; + if (chunks is { Count: > 0 }) + { + var context = string.Join("\n---\n", chunks); + prompt = $"Using ONLY the following passages, answer the question concisely.\n\nPassages:\n{context}\n\nQuestion: {query}\n\nAnswer:"; + } + else + { + prompt = $"Answer the following question concisely from your general knowledge.\n\nQuestion: {query}\n\nAnswer:"; + } + + var response = await _chatClient.GetResponseAsync(prompt, + new ChatOptions { MaxOutputTokens = 500 }, ct); + return response.Text ?? ""; + } + + private async Task<(int Relevance, int Faithfulness, string Critique)> SelfCritiqueAsync( + string query, string answer, List? chunks, CancellationToken ct) + { + var contextSection = chunks is { Count: > 0 } + ? "Retrieved passages:\n" + string.Join("\n---\n", chunks) + "\n\n" + : ""; + + var prompt = "Score the following answer from 1 (worst) to 5 (best).\n\n" + + $"Question: \"{query}\"\n\n{contextSection}" + + $"Answer: \"{answer}\"\n\n" + + "Dimensions:\n- relevance: Does it address the question?\n- faithfulness: Is it supported by the passages?\n\n" + + "Return ONLY valid JSON: {\"relevance\": 4, \"faithfulness\": 5, \"critique\": \"summary\"}"; + + try + { + var response = await _chatClient.GetResponseAsync(prompt, + new ChatOptions { MaxOutputTokens = 200, ResponseFormat = ChatResponseFormat.Json }, ct); + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + + return ( + Math.Clamp(result?.Relevance ?? 3, 1, 5), + Math.Clamp(result?.Faithfulness ?? 3, 1, 5), + result?.Critique ?? ""); + } + catch + { + return (3, 3, "critique failed"); + } + } + + private sealed class RetrievalDecisionResponse + { + [JsonPropertyName("needsRetrieval")] + public bool NeedsRetrieval { get; set; } + [JsonPropertyName("reasoning")] + public string Reasoning { get; set; } = ""; + } + + private sealed class CritiqueResponse + { + [JsonPropertyName("relevance")] + public int Relevance { get; set; } + [JsonPropertyName("faithfulness")] + public int Faithfulness { get; set; } + [JsonPropertyName("critique")] + public string Critique { get; set; } = ""; + } +} + +/// Result of the Self-RAG flow. +public sealed class SelfRagResult +{ + /// The generated answer. + public required string Answer { get; init; } + + /// Whether the system decided retrieval was needed. + public bool NeedsRetrieval { get; init; } + + /// LLM reasoning for the retrieval decision. + public string RetrievalReasoning { get; init; } = ""; + + /// Relevance score (1-5). + public int Relevance { get; init; } + + /// Faithfulness score (1-5). + public int Faithfulness { get; init; } + + /// Average of relevance and faithfulness. + public double AverageScore { get; init; } + + /// Self-critique summary. + public string Critique { get; init; } = ""; + + /// The retrieved chunks used for generation. + public List RetrievedChunks { get; init; } = []; +} diff --git a/MEDI/src/DataRetrieval/SpeculativeRagOrchestrator.cs b/MEDI/src/DataRetrieval/SpeculativeRagOrchestrator.cs new file mode 100644 index 0000000..51d1a0e --- /dev/null +++ b/MEDI/src/DataRetrieval/SpeculativeRagOrchestrator.cs @@ -0,0 +1,219 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.VectorData; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Speculative RAG orchestrator — parallel drafting with a small model + verification by a large model. +/// Leverages .NET's for true concurrent inference. +/// +/// +/// Uses keyed DI: register a "drafter" (small/fast model) and "verifier" (large/accurate model). +/// .NET's async/await + Task.WhenAll enables genuine concurrent LLM calls without GIL limitations. +/// +public class SpeculativeRagOrchestrator +{ + private readonly IChatClient _drafterClient; + private readonly IChatClient _verifierClient; + private readonly ILogger? _logger; + + /// Number of parallel drafts to generate. + public int DraftCount { get; init; } = 3; + + /// Maximum tokens for each draft response. + public int DraftMaxTokens { get; init; } = 500; + + /// Maximum tokens for the verification response. + public int VerifyMaxTokens { get; init; } = 300; + + /// + /// Initializes a new instance of the class. + /// + /// Small/fast model for parallel drafting. + /// Large/accurate model for verification. + /// Optional logger factory. + public SpeculativeRagOrchestrator( + IChatClient drafterClient, + IChatClient verifierClient, + ILoggerFactory? loggerFactory = null) + { + _drafterClient = drafterClient ?? throw new ArgumentNullException(nameof(drafterClient)); + _verifierClient = verifierClient ?? throw new ArgumentNullException(nameof(verifierClient)); + _logger = loggerFactory?.CreateLogger(); + } + + /// + /// Runs the Speculative RAG flow: retrieve → parallel draft → verify → select best. + /// + public async Task GenerateAsync( + VectorStoreCollection collection, + string query, + Func contentSelector, + int topK = 5, + CancellationToken cancellationToken = default) + where TKey : notnull + where TRecord : class + { + var retrievedChunks = new List(); + await foreach (var hit in collection.SearchAsync(query, top: topK, cancellationToken: cancellationToken)) + { + if (hit.Record is not null) + retrievedChunks.Add(contentSelector(hit.Record)); + } + + _logger?.LogDebug("Speculative RAG: retrieved {Count} chunks", retrievedChunks.Count); + + var subsets = CreateChunkSubsets(retrievedChunks, DraftCount); + + var draftTasks = subsets.Select(async (subset, idx) => + { + var prompt = $"Based on these passages, answer the question briefly:\n\n" + + string.Join("\n---\n", subset) + + $"\n\nQuestion: {query}"; + + var options = new ChatOptions { MaxOutputTokens = DraftMaxTokens }; + var response = await _drafterClient.GetResponseAsync(prompt, options, cancellationToken); + return new Draft + { + Index = idx + 1, + Text = (response.Text ?? "").Trim(), + ChunkSubset = subset + }; + }).ToArray(); + + var startTime = Environment.TickCount64; + var drafts = await Task.WhenAll(draftTasks); + var parallelMs = Environment.TickCount64 - startTime; + + _logger?.LogInformation( + "Speculative RAG: {Count} drafts generated in {Ms}ms (parallel)", + drafts.Length, parallelMs); + + var verification = await VerifyDraftsAsync(query, drafts, cancellationToken); + + var bestDraft = drafts.FirstOrDefault(d => d.Index == verification.BestDraftIndex) + ?? drafts.First(); + + return new SpeculativeRagResult + { + Answer = bestDraft.Text, + BestDraftIndex = verification.BestDraftIndex, + Confidence = verification.Confidence, + VerificationReasoning = verification.Reasoning, + Drafts = drafts.Select(d => new DraftResult + { + Index = d.Index, + Text = d.Text + }).ToList(), + ParallelDraftMs = parallelMs, + RetrievedChunks = retrievedChunks + }; + } + + private static List CreateChunkSubsets(List chunks, int subsetCount) + { + if (chunks.Count == 0) + return Enumerable.Range(0, subsetCount).Select(_ => Array.Empty()).ToList(); + + var subsets = new List(); + var chunkCount = chunks.Count; + + for (int i = 0; i < subsetCount; i++) + { + var start = (int)Math.Round((double)i * chunkCount / subsetCount); + var end = Math.Min(start + Math.Max(2, chunkCount / subsetCount + 1), chunkCount); + subsets.Add(chunks.Skip(start).Take(end - start).ToArray()); + } + + return subsets; + } + + private async Task VerifyDraftsAsync( + string query, Draft[] drafts, CancellationToken ct) + { + var draftSummaries = drafts.Select(d => + { + var text = d.Text.Length > 300 ? d.Text[..300] + "..." : d.Text; + return $"Draft {d.Index}: {text}"; + }); + + var prompt = $"Question: \"{query}\"\n\n" + + string.Join("\n\n", draftSummaries) + + "\n\nEvaluate which draft is most accurate and complete. " + + "Return ONLY valid JSON: {\"bestDraftIndex\": 1, \"confidence\": 0.85, \"reasoning\": \"one sentence\"}"; + + try + { + var response = await _verifierClient.GetResponseAsync(prompt, + new ChatOptions + { + MaxOutputTokens = VerifyMaxTokens, + ResponseFormat = ChatResponseFormat.Json + }, ct); + + return JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options) ?? new VerificationResponse(); + } + catch + { + return new VerificationResponse { BestDraftIndex = 1, Confidence = 0.5, Reasoning = "verification failed" }; + } + } + + private sealed class Draft + { + public int Index { get; init; } + public string Text { get; init; } = ""; + public string[] ChunkSubset { get; init; } = []; + } + + private sealed class VerificationResponse + { + [JsonPropertyName("bestDraftIndex")] + public int BestDraftIndex { get; set; } = 1; + + [JsonPropertyName("confidence")] + public double Confidence { get; set; } + + [JsonPropertyName("reasoning")] + public string Reasoning { get; set; } = ""; + } +} + +/// Result of the Speculative RAG flow. +public sealed class SpeculativeRagResult +{ + /// The selected best answer. + public required string Answer { get; init; } + + /// Index (1-based) of the selected draft. + public int BestDraftIndex { get; init; } + + /// Verifier confidence (0-1). + public double Confidence { get; init; } + + /// Verifier reasoning. + public string VerificationReasoning { get; init; } = ""; + + /// All generated drafts. + public List Drafts { get; init; } = []; + + /// Time (ms) for parallel draft generation. + public long ParallelDraftMs { get; init; } + + /// Retrieved chunks used for drafting. + public List RetrievedChunks { get; init; } = []; +} + +/// Individual draft from the Speculative RAG drafting phase. +public sealed class DraftResult +{ + /// Draft index (1-based). + public int Index { get; init; } + + /// Draft text. + public string Text { get; init; } = ""; +} diff --git a/MEDI/src/DataRetrieval/TreeSearchRetriever.cs b/MEDI/src/DataRetrieval/TreeSearchRetriever.cs new file mode 100644 index 0000000..305b586 --- /dev/null +++ b/MEDI/src/DataRetrieval/TreeSearchRetriever.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.DataRetrieval; +using Microsoft.Extensions.VectorData; + +namespace CommunityToolkit.DataRetrieval; + +/// +/// Top-down tree traversal retriever — searches root → branch → leaf levels. +/// Alternative search paradigm to flat vector search, best for exploratory or thematic queries. +/// +/// +/// Requires chunks to have a Level metadata field (set by TreeIndexProcessor). +/// Traversal: find best root summary → find best branch under it → return leaf chunks under that branch. +/// +public class TreeSearchRetriever : RetrievalQueryProcessor +{ + /// Number of results to retrieve at each level. + public int ResultsPerLevel { get; init; } = 3; + + /// + public override Task ProcessAsync( + RetrievalQuery query, CancellationToken cancellationToken = default) + { + query.Metadata["search_paradigm"] = "TreeTraversal"; + query.Metadata["results_per_level"] = ResultsPerLevel; + return Task.FromResult(query); + } + + /// + /// Performs top-down tree traversal search: root → branches → leaves. + /// + public static async Task> TraverseAsync( + VectorStoreCollection collection, + string query, + Func contentSelector, + Func levelSelector, + Func parentIdSelector, + Func idSelector, + int topK = 5, + CancellationToken cancellationToken = default) + where TKey : notnull + where TRecord : class + { + var results = new List(); + + var allResults = new List<(TRecord Record, double Score, int Level)>(); + await foreach (var hit in collection.SearchAsync(query, top: topK * 3, cancellationToken: cancellationToken)) + { + if (hit.Record is not null) + { + var level = levelSelector(hit.Record); + allResults.Add((hit.Record, hit.Score ?? 0, level)); + } + } + + var roots = allResults.Where(r => r.Level == 2).OrderByDescending(r => r.Score).Take(topK); + var branches = allResults.Where(r => r.Level == 1).OrderByDescending(r => r.Score).Take(topK); + var leaves = allResults.Where(r => r.Level == 0).OrderByDescending(r => r.Score).Take(topK); + + foreach (var item in roots.Concat(branches).Concat(leaves)) + { + var chunk = new RetrievalChunk(contentSelector(item.Record), item.Score); + chunk.Record["level"] = item.Level; + chunk.Record["id"] = idSelector(item.Record); + chunk.Record["parent_id"] = parentIdSelector(item.Record); + results.Add(chunk); + } + + return results.OrderByDescending(r => r.Score).Take(topK).ToList(); + } +} diff --git a/nuget.config b/nuget.config index 1f5c7e6..039ca38 100644 --- a/nuget.config +++ b/nuget.config @@ -4,6 +4,7 @@ + @@ -12,5 +13,8 @@ + + + From 01027c687a80aec2e889e4e852973a15454845c2 Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Tue, 5 May 2026 10:38:34 -0400 Subject: [PATCH 3/7] Add AsRetriever() builder terminal Adds a terminal method to RetrievalPipelineBuilder that registers a VectorStoreRetriever as IRetriever in DI. This enables the builder pattern: services.AddRetrievalPipeline(b => { b.AddQueryProcessor(...); }).AsRetriever(...); Also exposes Services property on builder so the terminal can register the retriever in the service collection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ataRetrievalServiceCollectionExtensions.cs | 4 +- .../DataRetrieval/RetrievalPipelineBuilder.cs | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs b/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs index 16e07d2..6756e86 100644 --- a/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs +++ b/MEDI/src/DataRetrieval/DataRetrievalServiceCollectionExtensions.cs @@ -25,7 +25,7 @@ public static class DataRetrievalServiceCollectionExtensions public static RetrievalPipelineBuilder AddRetrievalPipeline( this IServiceCollection services) { - var pipelineBuilder = new RetrievalPipelineBuilder(); + var pipelineBuilder = new RetrievalPipelineBuilder { Services = services }; services.AddSingleton(sp => { @@ -52,7 +52,7 @@ public static RetrievalPipelineBuilder AddRetrievalPipeline( this IServiceCollection services, Func pipelineFactory) { - var pipelineBuilder = new RetrievalPipelineBuilder(); + var pipelineBuilder = new RetrievalPipelineBuilder { Services = services }; services.AddSingleton(sp => { diff --git a/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs b/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs index 7c876b8..1333cc4 100644 --- a/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs +++ b/MEDI/src/DataRetrieval/RetrievalPipelineBuilder.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.DataRetrieval; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.VectorData; namespace CommunityToolkit.DataRetrieval; @@ -12,6 +13,7 @@ public class RetrievalPipelineBuilder { internal List> QueryProcessorFactories { get; } = []; internal List> ResultProcessorFactories { get; } = []; + internal IServiceCollection? Services { get; set; } /// Adds a query processor resolved from DI via . public RetrievalPipelineBuilder UseQueryProcessor() where T : RetrievalQueryProcessor @@ -105,6 +107,44 @@ public RetrievalPipelineBuilder UseCrag(Action? configure = null) }); return this; } + + /// + /// Registers an singleton that binds the configured pipeline + /// to a specific vector store collection. This is a terminal method — call it after + /// composing all processors. + /// + /// The vector store key type. + /// The vector store record type. + /// Factory to resolve the vector store collection from DI. + /// Optional function to extract text content from a record. + /// + /// + /// builder.Services.AddRetrievalPipeline() + /// .UseLlmReranking() + /// .AsRetriever<string, Article>( + /// sp => sp.GetRequiredService<VectorStoreCollection<string, Article>>(), + /// record => record.Content); + /// + /// + public void AsRetriever( + Func> collectionFactory, + Func? contentSelector = null) + where TKey : notnull + where TRecord : class + { + if (Services is null) + { + throw new InvalidOperationException( + "AsRetriever can only be called on a builder returned by AddRetrievalPipeline."); + } + + Services.AddSingleton(sp => + { + var pipeline = sp.GetRequiredService(); + var collection = collectionFactory(sp); + return pipeline.AsRetriever(collection, contentSelector); + }); + } } /// Options for multi-query expansion. From c4b01ff2135c1175b293d9529d6faf08f5a0422f Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Tue, 5 May 2026 11:44:04 -0400 Subject: [PATCH 4/7] Add CommunityToolkit.DataIngestion package with LLM chunk processors Port 4 LLM-powered ingestion chunk processors from MEDIExtensions: - EntityExtractionProcessor: NER via LLM (people, orgs, tech, versions) - TopicClassificationProcessor: Configurable taxonomy classification - HypotheticalQueryProcessor: Reverse HyDE question generation - TreeIndexProcessor: RAPTOR-style tree index (leaf/branch/root summaries) Includes: - IngestionPipelineBuilder with fluent builder extensions - DataIngestionServiceCollectionExtensions for DI registration - MetadataKeys constants (FDG compliance) - Optional ILogger on all processors (FDG compliance) - JsonDefaults internal utility Implements IngestionChunkProcessor from the stable Microsoft.Extensions.DataIngestion package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MEDI/src/DataIngestion/DataIngestion.csproj | 25 ++++ ...ataIngestionServiceCollectionExtensions.cs | 38 +++++ .../EntityExtractionProcessor.cs | 105 +++++++++++++ .../HypotheticalQueryProcessor.cs | 113 ++++++++++++++ .../DataIngestion/IngestionPipelineBuilder.cs | 140 ++++++++++++++++++ MEDI/src/DataIngestion/JsonDefaults.cs | 13 ++ MEDI/src/DataIngestion/MetadataKeys.cs | 52 +++++++ .../TopicClassificationProcessor.cs | 99 +++++++++++++ MEDI/src/DataIngestion/TreeIndexProcessor.cs | 140 ++++++++++++++++++ 9 files changed, 725 insertions(+) create mode 100644 MEDI/src/DataIngestion/DataIngestion.csproj create mode 100644 MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs create mode 100644 MEDI/src/DataIngestion/EntityExtractionProcessor.cs create mode 100644 MEDI/src/DataIngestion/HypotheticalQueryProcessor.cs create mode 100644 MEDI/src/DataIngestion/IngestionPipelineBuilder.cs create mode 100644 MEDI/src/DataIngestion/JsonDefaults.cs create mode 100644 MEDI/src/DataIngestion/MetadataKeys.cs create mode 100644 MEDI/src/DataIngestion/TopicClassificationProcessor.cs create mode 100644 MEDI/src/DataIngestion/TreeIndexProcessor.cs diff --git a/MEDI/src/DataIngestion/DataIngestion.csproj b/MEDI/src/DataIngestion/DataIngestion.csproj new file mode 100644 index 0000000..8b70edd --- /dev/null +++ b/MEDI/src/DataIngestion/DataIngestion.csproj @@ -0,0 +1,25 @@ + + + 1.0.0-preview.1 + CommunityToolkit.DataIngestion + $(AssemblyName) + net10.0;net8.0 + + LLM-powered chunk processors for Microsoft.Extensions.DataIngestion pipelines + LLM-powered ingestion chunk processors including entity extraction, topic classification, hypothetical query generation (reverse HyDE), and RAPTOR-style tree indexing for advanced RAG applications. + true + + + + + + + + + + + + + + + diff --git a/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs b/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs new file mode 100644 index 0000000..782ba5b --- /dev/null +++ b/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.DependencyInjection; + +namespace CommunityToolkit.DataIngestion; + +/// +/// Extension methods for registering ingestion pipeline builders with dependency injection. +/// +public static class DataIngestionServiceCollectionExtensions +{ + /// + /// Registers an singleton for composing + /// ingestion pipelines with LLM-powered chunk processors. + /// + /// + /// + /// builder.Services.AddIngestionPipeline<string>() + /// .UseEntityExtraction() + /// .UseTopicClassification(o => o.Taxonomy = ["web", "data", "security"]) + /// .UseHypotheticalQueries(o => o.QuestionsPerChunk = 5) + /// .UseTreeIndex(); + /// + /// + public static IngestionPipelineBuilder AddIngestionPipeline( + this IServiceCollection services) + { + var pipelineBuilder = new IngestionPipelineBuilder(); + services.AddSingleton(pipelineBuilder); + return pipelineBuilder; + } + + /// + /// Registers an with string content type (the common case). + /// + public static IngestionPipelineBuilder AddIngestionPipeline( + this IServiceCollection services) + => services.AddIngestionPipeline(); +} diff --git a/MEDI/src/DataIngestion/EntityExtractionProcessor.cs b/MEDI/src/DataIngestion/EntityExtractionProcessor.cs new file mode 100644 index 0000000..8f2bbd4 --- /dev/null +++ b/MEDI/src/DataIngestion/EntityExtractionProcessor.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace CommunityToolkit.DataIngestion; + +/// +/// An that uses an LLM to extract named entities +/// (people, organizations, technologies, versions) from each chunk and stores them as +/// metadata for downstream filtered vector search. +/// +public sealed class EntityExtractionProcessor : IngestionChunkProcessor +{ + private readonly IChatClient _chatClient; + private readonly ILogger _logger; + + private static readonly string[] EntityKeys = + [MetadataKeys.EntitiesPeople, MetadataKeys.EntitiesOrganizations, MetadataKeys.EntitiesTechnologies, MetadataKeys.EntitiesVersions]; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client used for entity extraction. + /// Optional logger for diagnostics. + public EntityExtractionProcessor(IChatClient chatClient, ILogger? logger = null) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + _logger = logger ?? NullLogger.Instance; + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IAsyncEnumerable> chunks, + [EnumeratorCancellation] CancellationToken ct = default) + { + await foreach (var chunk in chunks.WithCancellation(ct)) + { + try + { + var prompt = $$""" + Extract named entities from the following text. + Return ONLY valid JSON matching: {"people": ["name"], "organizations": ["org"], "technologies": ["tech"], "versions": ["v1"]} + Use empty arrays [] when none found for a category. + + Text: + {{chunk.Content}} + """; + + var options = new ChatOptions + { + MaxOutputTokens = 300, + ResponseFormat = ChatResponseFormat.Json + }; + + var response = await _chatClient.GetResponseAsync(prompt, options, ct); + var entities = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + + if (entities is not null) + { + chunk.Metadata[MetadataKeys.EntitiesPeople] = string.Join(", ", entities.People ?? []); + chunk.Metadata[MetadataKeys.EntitiesOrganizations] = string.Join(", ", entities.Organizations ?? []); + chunk.Metadata[MetadataKeys.EntitiesTechnologies] = string.Join(", ", entities.Technologies ?? []); + chunk.Metadata[MetadataKeys.EntitiesVersions] = string.Join(", ", entities.Versions ?? []); + } + else + { + SetEmptyEntities(chunk); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Entity extraction failed for chunk; using empty defaults."); + SetEmptyEntities(chunk); + } + + yield return chunk; + } + } + + private static void SetEmptyEntities(IngestionChunk chunk) + { + foreach (var key in EntityKeys) + chunk.Metadata[key] = ""; + } + + private sealed class EntityResponse + { + [JsonPropertyName("people")] + public string[]? People { get; set; } + + [JsonPropertyName("organizations")] + public string[]? Organizations { get; set; } + + [JsonPropertyName("technologies")] + public string[]? Technologies { get; set; } + + [JsonPropertyName("versions")] + public string[]? Versions { get; set; } + } +} diff --git a/MEDI/src/DataIngestion/HypotheticalQueryProcessor.cs b/MEDI/src/DataIngestion/HypotheticalQueryProcessor.cs new file mode 100644 index 0000000..35bde79 --- /dev/null +++ b/MEDI/src/DataIngestion/HypotheticalQueryProcessor.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace CommunityToolkit.DataIngestion; + +/// +/// An that generates hypothetical questions each chunk +/// could answer, storing them as additional retrieval vectors in the same collection. +/// This is "reverse HyDE" — bridging the query-document gap at ingestion time. +/// +/// +/// For each original chunk, yields: +/// +/// The original chunk (with chunk_type=original metadata) +/// N question chunks (with chunk_type=hypothetical_query and parent_chunk_id) +/// +/// Question chunks contain the question text as their Content — the embedding model creates +/// vectors in "question space," directly matching user queries. +/// +public sealed class HypotheticalQueryProcessor : IngestionChunkProcessor +{ + private readonly IChatClient _chatClient; + private readonly int _questionsPerChunk; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client used for question generation. + /// Number of hypothetical questions to generate per chunk (default: 3). + /// Optional logger for diagnostics. + public HypotheticalQueryProcessor(IChatClient chatClient, int questionsPerChunk = 3, ILogger? logger = null) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + _questionsPerChunk = questionsPerChunk; + _logger = logger ?? NullLogger.Instance; + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IAsyncEnumerable> chunks, + [EnumeratorCancellation] CancellationToken ct = default) + { + await foreach (var chunk in chunks.WithCancellation(ct)) + { + var chunkId = chunk.Content.GetHashCode().ToString(); + + // Yield original chunk first (with consistent metadata keys) + chunk.Metadata[MetadataKeys.ChunkType] = MetadataKeys.ChunkTypeOriginal; + chunk.Metadata[MetadataKeys.ParentChunkId] = ""; + chunk.Metadata[MetadataKeys.HypotheticalQuestions] = ""; + yield return chunk; + + // Generate hypothetical questions (best-effort) + var questionChunks = new List>(); + try + { + var questions = await GenerateQuestionsAsync(chunk.Content, ct); + chunk.Metadata[MetadataKeys.HypotheticalQuestions] = string.Join(" | ", questions); + + foreach (var question in questions) + { + var qChunk = new IngestionChunk(question, chunk.Document, chunk.Context); + qChunk.Metadata[MetadataKeys.ChunkType] = MetadataKeys.ChunkTypeHypotheticalQuery; + qChunk.Metadata[MetadataKeys.ParentChunkId] = chunkId; + qChunk.Metadata[MetadataKeys.HypotheticalQuestions] = ""; + questionChunks.Add(qChunk); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Hypothetical question generation failed for chunk; continuing with original only."); + } + + foreach (var qChunk in questionChunks) + yield return qChunk; + } + } + + private async Task> GenerateQuestionsAsync(string chunkContent, CancellationToken ct) + { + var prompt = $"Generate exactly {_questionsPerChunk} questions that the following " + + "text passage answers.\n" + + "Return ONLY valid JSON matching: {\"questions\": [\"question1?\", \"question2?\", \"question3?\"]}" + + $"\n\nPassage:\n{chunkContent}"; + + var response = await _chatClient.GetResponseAsync(prompt, + new ChatOptions + { + MaxOutputTokens = 200, + ResponseFormat = ChatResponseFormat.Json + }, ct); + + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + + return (result?.Questions ?? []) + .Where(q => !string.IsNullOrWhiteSpace(q) && q.Length > 10) + .Take(_questionsPerChunk) + .ToList(); + } + + private sealed class QuestionsResponse + { + [JsonPropertyName("questions")] + public List Questions { get; set; } = []; + } +} diff --git a/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs b/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs new file mode 100644 index 0000000..72baf43 --- /dev/null +++ b/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs @@ -0,0 +1,140 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace CommunityToolkit.DataIngestion; + +/// +/// Fluent builder for composing instances with +/// document and chunk processors. Registered via +/// . +/// +/// The chunk content type (typically string). +public class IngestionPipelineBuilder +{ + internal List> DocumentProcessorFactories { get; } = []; + internal List>> ChunkProcessorFactories { get; } = []; + + /// Adds a document processor created by the given factory. + public IngestionPipelineBuilder UseDocumentProcessor( + Func factory) + { + DocumentProcessorFactories.Add(factory); + return this; + } + + /// Adds a document processor resolved from DI. + public IngestionPipelineBuilder UseDocumentProcessor() + where TProcessor : IngestionDocumentProcessor + { + DocumentProcessorFactories.Add(sp => + ActivatorUtilities.CreateInstance(sp)); + return this; + } + + /// Adds a chunk processor created by the given factory. + public IngestionPipelineBuilder UseChunkProcessor( + Func> factory) + { + ChunkProcessorFactories.Add(factory); + return this; + } + + /// Adds a chunk processor resolved from DI. + public IngestionPipelineBuilder UseChunkProcessor() + where TProcessor : IngestionChunkProcessor + { + ChunkProcessorFactories.Add(sp => + ActivatorUtilities.CreateInstance(sp)); + return this; + } + + /// Adds entity extraction (people, organizations, technologies, versions) to chunks. + public IngestionPipelineBuilder UseEntityExtraction() + { + ChunkProcessorFactories.Add(sp => + (IngestionChunkProcessor)(object)new EntityExtractionProcessor( + sp.GetRequiredService(), + sp.GetService>())); + return this; + } + + /// Adds topic classification with a configurable taxonomy. + public IngestionPipelineBuilder UseTopicClassification( + Action? configure = null) + { + ChunkProcessorFactories.Add(sp => + { + var options = new TopicClassificationOptions(); + configure?.Invoke(options); + return (IngestionChunkProcessor)(object)new TopicClassificationProcessor( + sp.GetRequiredService(), + options.Taxonomy, + sp.GetService>()); + }); + return this; + } + + /// Adds hypothetical query generation for reverse-HyDE chunk enrichment. + public IngestionPipelineBuilder UseHypotheticalQueries( + Action? configure = null) + { + ChunkProcessorFactories.Add(sp => + { + var options = new HypotheticalQueryOptions(); + configure?.Invoke(options); + return (IngestionChunkProcessor)(object)new HypotheticalQueryProcessor( + sp.GetRequiredService(), + options.QuestionsPerChunk, + sp.GetService>()); + }); + return this; + } + + /// Adds RAPTOR-style tree index generation (leaf → branch → root summaries). + public IngestionPipelineBuilder UseTreeIndex() + { + ChunkProcessorFactories.Add(sp => + (IngestionChunkProcessor)(object)new TreeIndexProcessor( + sp.GetRequiredService(), + sp.GetService>())); + return this; + } + + /// + /// Creates a configured with all registered processors. + /// The caller is responsible for disposing the returned pipeline. + /// + public IngestionPipeline Build( + IServiceProvider serviceProvider, + IngestionDocumentReader reader, + IngestionChunker chunker, + IngestionChunkWriter writer, + ILoggerFactory? loggerFactory = null) + { + var pipeline = new IngestionPipeline(reader, chunker, writer, loggerFactory: loggerFactory); + + foreach (var factory in DocumentProcessorFactories) + pipeline.DocumentProcessors.Add(factory(serviceProvider)); + + foreach (var factory in ChunkProcessorFactories) + pipeline.ChunkProcessors.Add(factory(serviceProvider)); + + return pipeline; + } +} + +/// Options for . +public class TopicClassificationOptions +{ + /// Valid topic labels for classification. + public string[] Taxonomy { get; set; } = ["web", "data", "performance", "security", "architecture"]; +} + +/// Options for . +public class HypotheticalQueryOptions +{ + /// Number of questions to generate per chunk. + public int QuestionsPerChunk { get; set; } = 3; +} diff --git a/MEDI/src/DataIngestion/JsonDefaults.cs b/MEDI/src/DataIngestion/JsonDefaults.cs new file mode 100644 index 0000000..fb74687 --- /dev/null +++ b/MEDI/src/DataIngestion/JsonDefaults.cs @@ -0,0 +1,13 @@ +using System.Text.Json; + +namespace CommunityToolkit.DataIngestion; + +/// Shared JSON serialization defaults for LLM response parsing. +internal static class JsonDefaults +{ + public static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; +} diff --git a/MEDI/src/DataIngestion/MetadataKeys.cs b/MEDI/src/DataIngestion/MetadataKeys.cs new file mode 100644 index 0000000..9b992ee --- /dev/null +++ b/MEDI/src/DataIngestion/MetadataKeys.cs @@ -0,0 +1,52 @@ +namespace CommunityToolkit.DataIngestion; + +/// +/// Well-known metadata keys written by the ingestion chunk processors in this package. +/// +public static class MetadataKeys +{ + /// Comma-separated list of people entities extracted from the chunk. + public const string EntitiesPeople = "entities_people"; + + /// Comma-separated list of organization entities extracted from the chunk. + public const string EntitiesOrganizations = "entities_organizations"; + + /// Comma-separated list of technology entities extracted from the chunk. + public const string EntitiesTechnologies = "entities_technologies"; + + /// Comma-separated list of version strings extracted from the chunk. + public const string EntitiesVersions = "entities_versions"; + + /// Primary topic label assigned by topic classification. + public const string TopicPrimary = "topic_primary"; + + /// Comma-separated secondary topic labels assigned by topic classification. + public const string TopicSecondary = "topic_secondary"; + + /// The type of chunk (original, hypothetical_query, branch_summary, root_summary). + public const string ChunkType = "chunk_type"; + + /// The identifier of the parent chunk (for hypothetical query chunks). + public const string ParentChunkId = "parent_chunk_id"; + + /// Pipe-delimited hypothetical questions generated for a chunk. + public const string HypotheticalQuestions = "hypothetical_questions"; + + /// Tree hierarchy level (0 = leaf, 1 = branch, 2 = root). + public const string Level = "level"; + + /// The identifier of the parent node in the tree index hierarchy. + public const string ParentId = "parent_id"; + + /// Chunk type value for original (leaf) content chunks. + public const string ChunkTypeOriginal = "original"; + + /// Chunk type value for hypothetical query expansion chunks. + public const string ChunkTypeHypotheticalQuery = "hypothetical_query"; + + /// Chunk type value for document-level (branch) summary chunks. + public const string ChunkTypeBranchSummary = "branch_summary"; + + /// Chunk type value for corpus-level (root) summary chunks. + public const string ChunkTypeRootSummary = "root_summary"; +} diff --git a/MEDI/src/DataIngestion/TopicClassificationProcessor.cs b/MEDI/src/DataIngestion/TopicClassificationProcessor.cs new file mode 100644 index 0000000..feefa5e --- /dev/null +++ b/MEDI/src/DataIngestion/TopicClassificationProcessor.cs @@ -0,0 +1,99 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace CommunityToolkit.DataIngestion; + +/// +/// An that classifies each chunk into primary and +/// optional secondary topics from a configurable taxonomy. Stores topic_primary +/// (single label) and topic_secondary (comma-separated) as chunk metadata. +/// +public sealed class TopicClassificationProcessor : IngestionChunkProcessor +{ + private readonly IChatClient _chatClient; + private readonly string[] _taxonomy; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client used for topic classification. + /// Valid topic labels (e.g., "web", "data", "security", "performance", "architecture"). + /// Optional logger for diagnostics. + public TopicClassificationProcessor(IChatClient chatClient, string[] taxonomy, ILogger? logger = null) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + _taxonomy = taxonomy ?? throw new ArgumentNullException(nameof(taxonomy)); + _logger = logger ?? NullLogger.Instance; + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IAsyncEnumerable> chunks, + [EnumeratorCancellation] CancellationToken ct = default) + { + await foreach (var chunk in chunks.WithCancellation(ct)) + { + string primary = "unknown"; + string secondary = ""; + + try + { + var prompt = BuildPrompt(chunk.Content); + var options = new ChatOptions + { + MaxOutputTokens = 100, + ResponseFormat = ChatResponseFormat.Json + }; + + var response = await _chatClient.GetResponseAsync(prompt, options, ct); + var parsed = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options) + ?? new TopicResponse(); + + if (_taxonomy.Contains(parsed.Primary)) + { + primary = parsed.Primary; + var validSecondary = (parsed.Secondary ?? []) + .Where(t => _taxonomy.Contains(t) && t != primary) + .Distinct(); + secondary = string.Join(",", validSecondary); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Topic classification failed for chunk; defaulting to 'unknown'."); + } + + chunk.Metadata[MetadataKeys.TopicPrimary] = primary; + chunk.Metadata[MetadataKeys.TopicSecondary] = secondary; + + yield return chunk; + } + } + + private string BuildPrompt(string text) => $$""" + Classify this text into topics from: [{{string.Join(", ", _taxonomy.Select(t => $"\"{t}\""))}}]. + + Return ONLY valid JSON (no extra text): + {"primary": "topic", "secondary": ["topic2", "topic3"]} + + If the text fits only one topic, return an empty secondary array. + + Text: {{text}} + """; + + private sealed class TopicResponse + { + [JsonPropertyName("primary")] + public string Primary { get; set; } = ""; + + [JsonPropertyName("secondary")] + public List Secondary { get; set; } = []; + } +} diff --git a/MEDI/src/DataIngestion/TreeIndexProcessor.cs b/MEDI/src/DataIngestion/TreeIndexProcessor.cs new file mode 100644 index 0000000..af5e7b6 --- /dev/null +++ b/MEDI/src/DataIngestion/TreeIndexProcessor.cs @@ -0,0 +1,140 @@ +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace CommunityToolkit.DataIngestion; + +/// +/// A RAPTOR-style tree index processor that generates summary nodes at multiple levels. +/// +/// +/// For each document's chunks, generates: +/// +/// Level 1 (Branch): LLM summary of all chunks within the same document +/// Level 2 (Root): LLM summary of all branch summaries (corpus overview) +/// +/// Summary nodes are injected into the same chunk stream with level metadata. +/// The vector store writer stores them in the same collection as leaf chunks. +/// Standard VectorStoreCollection.SearchAsync naturally returns a mix of leaf +/// chunks (specific detail) and summary nodes (broader context). +/// +public sealed class TreeIndexProcessor : IngestionChunkProcessor +{ + private readonly IChatClient _chatClient; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client used for summarization. + /// Optional logger for diagnostics. + public TreeIndexProcessor(IChatClient chatClient, ILogger? logger = null) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + _logger = logger ?? NullLogger.Instance; + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IAsyncEnumerable> chunks, + [EnumeratorCancellation] CancellationToken ct = default) + { + // Collect all chunks, grouping by source document for summarization + var chunksByDoc = new Dictionary>>(); + var allChunks = new List>(); + + await foreach (var chunk in chunks.WithCancellation(ct)) + { + // Mark as leaf node + chunk.Metadata[MetadataKeys.Level] = 0; + chunk.Metadata[MetadataKeys.ChunkType] = MetadataKeys.ChunkTypeOriginal; + chunk.Metadata[MetadataKeys.ParentId] = ""; + + allChunks.Add(chunk); + + var docId = SanitizeDocId(chunk.Document.Identifier); + if (!chunksByDoc.TryGetValue(docId, out var list)) + chunksByDoc[docId] = list = []; + list.Add(chunk); + } + + // Yield all original leaf chunks first + foreach (var chunk in allChunks) + yield return chunk; + + // Level 1 (Branch): Generate document-level summaries + var branchSummaries = new List<(string DocId, string Summary)>(); + foreach (var (docId, docChunks) in chunksByDoc) + { + var combinedText = string.Join("\n\n", docChunks.Select(c => c.Content)); + var summary = await SummarizeAsync( + $"Summarize the following text in 2-3 concise sentences. Capture key concepts and technologies.\n\n{combinedText}", ct); + + branchSummaries.Add((docId, summary)); + + // Update leaf chunks to reference their branch parent + foreach (var leaf in docChunks) + leaf.Metadata[MetadataKeys.ParentId] = $"branch-{docId}"; + + // Create and yield branch summary chunk + var branchChunk = new IngestionChunk(summary, docChunks[0].Document, docChunks[0].Context); + branchChunk.Metadata[MetadataKeys.Level] = 1; + branchChunk.Metadata[MetadataKeys.ChunkType] = MetadataKeys.ChunkTypeBranchSummary; + branchChunk.Metadata[MetadataKeys.ParentId] = "root"; + yield return branchChunk; + } + + // Level 2 (Root): Generate corpus-level summary + if (branchSummaries.Count > 0) + { + var allBranchText = string.Join("\n\n", + branchSummaries.Select(b => $"[{b.DocId}]: {b.Summary}")); + + var rootSummary = await SummarizeAsync( + "Write a single 2-3 sentence overview of the entire corpus:\n\n" + allBranchText, ct); + + var rootChunk = new IngestionChunk(rootSummary, allChunks[0].Document, allChunks[0].Context); + rootChunk.Metadata[MetadataKeys.Level] = 2; + rootChunk.Metadata[MetadataKeys.ChunkType] = MetadataKeys.ChunkTypeRootSummary; + rootChunk.Metadata[MetadataKeys.ParentId] = ""; + yield return rootChunk; + } + } + + private async Task SummarizeAsync(string prompt, CancellationToken ct) + { + var fullPrompt = prompt + "\nReturn ONLY valid JSON: {\"summary\": \"your summary here\"}"; + + try + { + var response = await _chatClient.GetResponseAsync(fullPrompt, + new ChatOptions + { + MaxOutputTokens = 200, + ResponseFormat = ChatResponseFormat.Json + }, ct); + + var result = JsonSerializer.Deserialize( + response.Text ?? "{}", JsonDefaults.Options); + return result?.Summary ?? "Summary unavailable"; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Tree index summarization failed; using placeholder."); + return "Summary unavailable"; + } + } + + private static string SanitizeDocId(string identifier) => + Path.GetFileNameWithoutExtension(identifier).Replace(" ", "-").ToLowerInvariant(); + + private sealed class SummaryResponse + { + [JsonPropertyName("summary")] + public string Summary { get; set; } = ""; + } +} From 4aacd112513d1636106bf8554bf8169c38d7a558 Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Tue, 5 May 2026 12:10:23 -0400 Subject: [PATCH 5/7] Add DataRetrieval unit tests (48 tests, all passing) Port retrieval processor unit tests from MEDIExtensions with namespace remapping. Covers: MultiQueryExpander, LlmReranker, HydeQueryTransformer, CragValidator, AdaptiveRouter, SelfRagOrchestrator, SpeculativeRagOrchestrator, TreeSearchRetriever. Includes test utilities (TestChatClient, TestVectorStoreCollection) for deterministic LLM-free testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DataRetrieval.UnitTests.csproj | 34 ++++ .../Retrieval/AdaptiveRouterTests.cs | 79 ++++++++++ .../Retrieval/CragValidatorTests.cs | 122 +++++++++++++++ .../Retrieval/HydeQueryTransformerTests.cs | 87 +++++++++++ .../Retrieval/LlmRerankerTests.cs | 118 ++++++++++++++ .../Retrieval/MultiQueryExpanderTests.cs | 82 ++++++++++ .../Retrieval/SelfRagOrchestratorTests.cs | 147 ++++++++++++++++++ .../SpeculativeRagOrchestratorTests.cs | 110 +++++++++++++ .../Retrieval/TreeSearchRetrieverTests.cs | 56 +++++++ .../Utils/TestChatClient.cs | 45 ++++++ .../Utils/TestVectorStoreCollection.cs | 90 +++++++++++ 11 files changed, 970 insertions(+) create mode 100644 MEDI/test/DataRetrieval.UnitTests/DataRetrieval.UnitTests.csproj create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/AdaptiveRouterTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/CragValidatorTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/HydeQueryTransformerTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/LlmRerankerTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/MultiQueryExpanderTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/SelfRagOrchestratorTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/SpeculativeRagOrchestratorTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Retrieval/TreeSearchRetrieverTests.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Utils/TestChatClient.cs create mode 100644 MEDI/test/DataRetrieval.UnitTests/Utils/TestVectorStoreCollection.cs diff --git a/MEDI/test/DataRetrieval.UnitTests/DataRetrieval.UnitTests.csproj b/MEDI/test/DataRetrieval.UnitTests/DataRetrieval.UnitTests.csproj new file mode 100644 index 0000000..7cde736 --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/DataRetrieval.UnitTests.csproj @@ -0,0 +1,34 @@ + + + + CommunityToolkit.DataRetrieval.UnitTests + CommunityToolkit.DataRetrieval.UnitTests + net10.0 + true + false + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/AdaptiveRouterTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/AdaptiveRouterTests.cs new file mode 100644 index 0000000..ecb855f --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/AdaptiveRouterTests.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class AdaptiveRouterTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", () => new AdaptiveRouter(null!)); + } + + [Theory] + [InlineData("vector")] + [InlineData("tree")] + [InlineData("entity")] + public async Task ProcessAsync_ValidParadigm_SetsMetadata(string paradigm) + { + using var client = TestChatClient.WithJsonResponse( + $$"""{"paradigm": "{{paradigm}}", "reasoning": "test reasoning"}"""); + var router = new AdaptiveRouter(client); + var query = new RetrievalQuery("test query"); + + var output = await router.ProcessAsync(query); + + Assert.Equal(paradigm, output.Metadata["search_paradigm"]); + } + + [Fact] + public async Task ProcessAsync_InvalidParadigm_DefaultsToVector() + { + using var client = TestChatClient.WithJsonResponse( + """{"paradigm": "unknown_paradigm", "reasoning": "bad"}"""); + var router = new AdaptiveRouter(client); + var query = new RetrievalQuery("test query"); + + var output = await router.ProcessAsync(query); + + Assert.Equal("vector", output.Metadata["search_paradigm"]); + } + + [Fact] + public async Task ProcessAsync_LlmThrows_DefaultsToVector() + { + using var client = TestChatClient.WithException(new InvalidOperationException("fail")); + var router = new AdaptiveRouter(client); + var query = new RetrievalQuery("test query"); + + var output = await router.ProcessAsync(query); + + Assert.Equal("vector", output.Metadata["search_paradigm"]); + } + + [Fact] + public async Task ProcessAsync_StoresReasoningWhenPresent() + { + using var client = TestChatClient.WithJsonResponse( + """{"paradigm": "tree", "reasoning": "broad question about architecture"}"""); + var router = new AdaptiveRouter(client); + var query = new RetrievalQuery("what are the design patterns used?"); + + var output = await router.ProcessAsync(query); + + Assert.Equal("broad question about architecture", output.Metadata["router_reasoning"]); + } + + [Fact] + public async Task ProcessAsync_NullReasoning_NoMetadataKey() + { + using var client = TestChatClient.WithJsonResponse("""{"paradigm": "vector"}"""); + var router = new AdaptiveRouter(client); + var query = new RetrievalQuery("test"); + + var output = await router.ProcessAsync(query); + + Assert.Equal("vector", output.Metadata["search_paradigm"]); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/CragValidatorTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/CragValidatorTests.cs new file mode 100644 index 0000000..975f59b --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/CragValidatorTests.cs @@ -0,0 +1,122 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class CragValidatorTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", () => new CragValidator(null!)); + } + + [Fact] + public async Task ProcessAsync_EmptyChunks_ReturnsIncorrectPath() + { + using var client = TestChatClient.WithJsonResponse("""{"score": 5}"""); + var validator = new CragValidator(client); + var query = new RetrievalQuery("test"); + var results = new RetrievalResults { Chunks = [] }; + + var output = await validator.ProcessAsync(results, query); + + Assert.Equal(0, output.Metadata["crag_score"]); + Assert.Equal("Incorrect", output.Metadata["crag_path"]); + Assert.True((bool)output.Metadata["low_confidence"]!); + } + + [Fact] + public async Task ProcessAsync_HighScore_ReturnsCorrectPath() + { + using var client = TestChatClient.WithJsonResponse("""{"score": 5, "reasoning": "great match"}"""); + var validator = new CragValidator(client); + var (results, query) = CreateResultsWithChunks(3); + + var output = await validator.ProcessAsync(results, query); + + Assert.Equal(5, output.Metadata["crag_score"]); + Assert.Equal("Correct", output.Metadata["crag_path"]); + Assert.False(output.Metadata.ContainsKey("low_confidence")); + } + + [Fact] + public async Task ProcessAsync_MidScore_ReturnsAmbiguousPath() + { + using var client = TestChatClient.WithJsonResponse("""{"score": 3, "reasoning": "partial match"}"""); + var validator = new CragValidator(client); + var (results, query) = CreateResultsWithChunks(3); + + var output = await validator.ProcessAsync(results, query); + + Assert.Equal(3, output.Metadata["crag_score"]); + Assert.Equal("Ambiguous", output.Metadata["crag_path"]); + Assert.True((bool)output.Metadata["needs_followup"]!); + } + + [Fact] + public async Task ProcessAsync_LowScore_ReturnsIncorrectAndClearsChunks() + { + using var client = TestChatClient.WithJsonResponse("""{"score": 1, "reasoning": "off topic"}"""); + var validator = new CragValidator(client); + var (results, query) = CreateResultsWithChunks(3); + + var output = await validator.ProcessAsync(results, query); + + Assert.Equal(1, output.Metadata["crag_score"]); + Assert.Equal("Incorrect", output.Metadata["crag_path"]); + Assert.True((bool)output.Metadata["low_confidence"]!); + Assert.Empty(output.Chunks); + } + + [Fact] + public async Task ProcessAsync_ScoreOutOfRange_ClampedToDefault() + { + using var client = TestChatClient.WithJsonResponse("""{"score": 99}"""); + var validator = new CragValidator(client); + var (results, query) = CreateResultsWithChunks(3); + + var output = await validator.ProcessAsync(results, query); + + // Score 99 is not in [1,5] → defaults to 3 (Ambiguous) + Assert.Equal(3, output.Metadata["crag_score"]); + Assert.Equal("Ambiguous", output.Metadata["crag_path"]); + } + + [Fact] + public async Task ProcessAsync_LlmThrows_DefaultsToAmbiguous() + { + using var client = TestChatClient.WithException(new InvalidOperationException("fail")); + var validator = new CragValidator(client); + var (results, query) = CreateResultsWithChunks(3); + + var output = await validator.ProcessAsync(results, query); + + Assert.Equal(3, output.Metadata["crag_score"]); + Assert.Equal("Ambiguous", output.Metadata["crag_path"]); + } + + [Fact] + public async Task ProcessAsync_StoresReasoningInMetadata() + { + using var client = TestChatClient.WithJsonResponse("""{"score": 4, "reasoning": "relevant results"}"""); + var validator = new CragValidator(client); + var (results, query) = CreateResultsWithChunks(3); + + var output = await validator.ProcessAsync(results, query); + + Assert.Equal("relevant results", output.Metadata["crag_reasoning"]); + } + + private static (RetrievalResults results, RetrievalQuery query) CreateResultsWithChunks(int count) + { + var query = new RetrievalQuery("test query"); + var results = new RetrievalResults + { + Chunks = Enumerable.Range(0, count) + .Select(i => new RetrievalChunk($"Chunk {i} content", 0.9 - i * 0.1)) + .ToList() + }; + return (results, query); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/HydeQueryTransformerTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/HydeQueryTransformerTests.cs new file mode 100644 index 0000000..4a2502b --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/HydeQueryTransformerTests.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class HydeQueryTransformerTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", () => new HydeQueryTransformer(null!)); + } + + [Fact] + public async Task ProcessAsync_ReplacesVariantsWithHypothetical() + { + using var client = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, + "Dependency injection in .NET uses the IServiceCollection to register services."))) + }; + var transformer = new HydeQueryTransformer(client); + var query = new RetrievalQuery("How does DI work in .NET?"); + + var output = await transformer.ProcessAsync(query); + + Assert.Single(output.Variants); + Assert.Contains("Dependency injection", output.Variants[0]); + Assert.Equal("How does DI work in .NET?", output.Text); // Original preserved + } + + [Fact] + public async Task ProcessAsync_StoresHydeMetadata() + { + using var client = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, + "A hypothetical answer about the topic."))) + }; + var transformer = new HydeQueryTransformer(client); + var query = new RetrievalQuery("some question"); + + var output = await transformer.ProcessAsync(query); + + Assert.True(output.Metadata.ContainsKey("hyde_hypothetical")); + Assert.Equal("A hypothetical answer about the topic.", output.Metadata["hyde_hypothetical"]); + } + + [Fact] + public async Task ProcessAsync_EmptyResponse_ReturnsOriginal() + { + using var client = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, " "))) + }; + var transformer = new HydeQueryTransformer(client); + var query = new RetrievalQuery("my question"); + + var output = await transformer.ProcessAsync(query); + + Assert.Single(output.Variants); + Assert.Equal("my question", output.Variants[0]); // Unchanged + } + + [Fact] + public async Task ProcessAsync_LlmThrows_ReturnsOriginal() + { + using var client = TestChatClient.WithException(new InvalidOperationException("timeout")); + var transformer = new HydeQueryTransformer(client); + var query = new RetrievalQuery("my question"); + + var output = await transformer.ProcessAsync(query); + + Assert.Single(output.Variants); + Assert.Equal("my question", output.Variants[0]); + Assert.False(output.Metadata.ContainsKey("hyde_hypothetical")); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/LlmRerankerTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/LlmRerankerTests.cs new file mode 100644 index 0000000..76232a3 --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/LlmRerankerTests.cs @@ -0,0 +1,118 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class LlmRerankerTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", () => new LlmReranker(null!)); + } + + [Fact] + public async Task ProcessAsync_TwoOrFewerChunks_ReturnsUnchanged() + { + using var client = TestChatClient.WithJsonResponse("""{"rankedIndices": [2, 1]}"""); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("chunk1", "chunk2"); + + var output = await reranker.ProcessAsync(results, query); + + Assert.Equal(2, output.Chunks.Count); + Assert.Equal("chunk1", output.Chunks[0].Content); + } + + [Fact] + public async Task ProcessAsync_ReranksBasedOnLlmResponse() + { + using var client = TestChatClient.WithJsonResponse("""{"rankedIndices": [3, 1, 2]}"""); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("A", "B", "C"); + + var output = await reranker.ProcessAsync(results, query); + + Assert.Equal("C", output.Chunks[0].Content); + Assert.Equal("A", output.Chunks[1].Content); + Assert.Equal("B", output.Chunks[2].Content); + } + + [Fact] + public async Task ProcessAsync_FiltersOutOfRangeIndices() + { + using var client = TestChatClient.WithJsonResponse("""{"rankedIndices": [99, 1, -1, 2]}"""); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("A", "B", "C"); + + var output = await reranker.ProcessAsync(results, query); + + Assert.Equal(2, output.Chunks.Count); + Assert.Equal("A", output.Chunks[0].Content); + Assert.Equal("B", output.Chunks[1].Content); + } + + [Fact] + public async Task ProcessAsync_DeduplicatesIndices() + { + using var client = TestChatClient.WithJsonResponse("""{"rankedIndices": [2, 2, 2, 1]}"""); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("A", "B", "C"); + + var output = await reranker.ProcessAsync(results, query); + + Assert.Equal(2, output.Chunks.Count); + Assert.Equal("B", output.Chunks[0].Content); + Assert.Equal("A", output.Chunks[1].Content); + } + + [Fact] + public async Task ProcessAsync_FallsBackToTopCandidatesOnInvalidJson() + { + using var client = TestChatClient.WithJsonResponse("not valid json"); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("A", "B", "C"); + + var output = await reranker.ProcessAsync(results, query); + + // Fallback: top MaxResults candidates returned as-is + Assert.True(output.Chunks.Count > 0); + Assert.Equal("A", output.Chunks[0].Content); + } + + [Fact] + public async Task ProcessAsync_FallsBackToTopCandidatesOnException() + { + using var client = TestChatClient.WithException(new InvalidOperationException("LLM down")); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("A", "B", "C"); + + var output = await reranker.ProcessAsync(results, query); + + Assert.True(output.Chunks.Count > 0); + Assert.Equal("A", output.Chunks[0].Content); + } + + [Fact] + public async Task ProcessAsync_SetsRerankedMetadata() + { + using var client = TestChatClient.WithJsonResponse("""{"rankedIndices": [1, 2, 3]}"""); + var reranker = new LlmReranker(client); + var (results, query) = CreateResults("A", "B", "C"); + + var output = await reranker.ProcessAsync(results, query); + + Assert.True((bool)output.Metadata["reranked"]!); + Assert.Equal(3, (int)output.Metadata["reranked_count"]!); + } + + private static (RetrievalResults results, RetrievalQuery query) CreateResults(params string[] contents) + { + var query = new RetrievalQuery("test query"); + var results = new RetrievalResults + { + Chunks = contents.Select((c, i) => new RetrievalChunk(c, 1.0 - i * 0.1)).ToList() + }; + return (results, query); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/MultiQueryExpanderTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/MultiQueryExpanderTests.cs new file mode 100644 index 0000000..62cd6e0 --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/MultiQueryExpanderTests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class MultiQueryExpanderTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", () => new MultiQueryExpander(null!)); + } + + [Fact] + public async Task ProcessAsync_AddsVariantsAfterOriginal() + { + using var client = TestChatClient.WithJsonResponse( + """{"variants": ["how to configure RAG", "RAG pipeline setup guide", "setting up retrieval augmented generation"]}"""); + var expander = new MultiQueryExpander(client); + var query = new RetrievalQuery("how to set up RAG"); + + var output = await expander.ProcessAsync(query); + + Assert.Equal("how to set up RAG", output.Variants[0]); // Original first + Assert.True(output.Variants.Count >= 2); // At least original + 1 variant + } + + [Fact] + public async Task ProcessAsync_FiltersShortVariants() + { + using var client = TestChatClient.WithJsonResponse( + """{"variants": ["short", "", "this is a valid query variant about RAG"]}"""); + var expander = new MultiQueryExpander(client); + var query = new RetrievalQuery("original query text"); + + var output = await expander.ProcessAsync(query); + + // "short" (5 chars) and "" are filtered out, only the valid variant remains + Assert.Equal(2, output.Variants.Count); // original + 1 valid + Assert.DoesNotContain("short", output.Variants); + } + + [Fact] + public async Task ProcessAsync_OriginalAlwaysFirst() + { + using var client = TestChatClient.WithJsonResponse( + """{"variants": ["variant one that is long enough", "variant two that is long enough"]}"""); + var expander = new MultiQueryExpander(client); + var query = new RetrievalQuery("my original question"); + + var output = await expander.ProcessAsync(query); + + Assert.Equal("my original question", output.Variants[0]); + } + + [Fact] + public async Task ProcessAsync_LlmThrows_ReturnsOriginalOnly() + { + using var client = TestChatClient.WithException(new InvalidOperationException("fail")); + var expander = new MultiQueryExpander(client); + var query = new RetrievalQuery("my question"); + + var output = await expander.ProcessAsync(query); + + Assert.Single(output.Variants); + Assert.Equal("my question", output.Variants[0]); + } + + [Fact] + public async Task ProcessAsync_LimitsToVariantCount() + { + using var client = TestChatClient.WithJsonResponse( + """{"variants": ["variant one is long enough", "variant two is long enough", "variant three is long enough", "variant four is long enough", "variant five is long enough"]}"""); + var expander = new MultiQueryExpander(client) { VariantCount = 2 }; + var query = new RetrievalQuery("test"); + + var output = await expander.ProcessAsync(query); + + // 1 original + at most 2 variants + Assert.True(output.Variants.Count <= 3); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/SelfRagOrchestratorTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/SelfRagOrchestratorTests.cs new file mode 100644 index 0000000..552da32 --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/SelfRagOrchestratorTests.cs @@ -0,0 +1,147 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class SelfRagOrchestratorTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", () => new SelfRagOrchestrator(null!)); + } + + [Fact] + public async Task GenerateAsync_NeedsRetrieval_RetrievesChunks() + { + int callCount = 0; + using var client = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, ct) => + { + callCount++; + string json = callCount switch + { + 1 => """{"needsRetrieval": true, "reasoning": "technical question"}""", + 2 => "The answer is based on retrieved context.", + 3 => """{"relevance": 5, "faithfulness": 5, "critique": "excellent"}""", + _ => "{}" + }; + return Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, json))); + } + }; + + var collection = new TestVectorStoreCollection(); + collection.AddSearchResult("relevant chunk", 0.9); + + var orchestrator = new SelfRagOrchestrator(client); + var result = await orchestrator.GenerateAsync( + collection, "How does DI work?", + r => (string)r["content"]!); + + Assert.True(result.NeedsRetrieval); + Assert.NotEmpty(result.RetrievedChunks); + Assert.NotEmpty(result.Answer); + } + + [Fact] + public async Task GenerateAsync_NoRetrieval_GeneratesFromKnowledge() + { + int callCount = 0; + using var client = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, ct) => + { + callCount++; + string json = callCount switch + { + 1 => """{"needsRetrieval": false, "reasoning": "general knowledge"}""", + 2 => "The answer from general knowledge.", + 3 => """{"relevance": 4, "faithfulness": 4, "critique": "good"}""", + _ => "{}" + }; + return Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, json))); + } + }; + + var collection = new TestVectorStoreCollection(); + var orchestrator = new SelfRagOrchestrator(client); + var result = await orchestrator.GenerateAsync( + collection, "What is water?", + r => (string)r["content"]!); + + Assert.False(result.NeedsRetrieval); + Assert.Empty(result.RetrievedChunks); + } + + [Fact] + public async Task GenerateAsync_LowScore_RetriesWithForcedRetrieval() + { + int callCount = 0; + using var client = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, ct) => + { + callCount++; + string json = callCount switch + { + 1 => """{"needsRetrieval": false, "reasoning": "try without"}""", + 2 => "Weak answer.", + 3 => """{"relevance": 1, "faithfulness": 1, "critique": "poor"}""", + 4 => "Better answer with context.", + 5 => """{"relevance": 4, "faithfulness": 4, "critique": "improved"}""", + _ => "{}" + }; + return Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, json))); + } + }; + + var collection = new TestVectorStoreCollection(); + collection.AddSearchResult("helpful context", 0.9); + + var orchestrator = new SelfRagOrchestrator(client) { AcceptanceThreshold = 3.0 }; + var result = await orchestrator.GenerateAsync( + collection, "complex question", + r => (string)r["content"]!); + + Assert.True(callCount >= 4, "Should have retried after low score"); + } + + [Fact] + public async Task GenerateAsync_MaxRetriesZero_NoRetryEvenIfLowScore() + { + int callCount = 0; + using var client = new TestChatClient + { + GetResponseAsyncCallback = (messages, options, ct) => + { + callCount++; + string json = callCount switch + { + 1 => """{"needsRetrieval": false, "reasoning": "try"}""", + 2 => "Bad answer.", + 3 => """{"relevance": 1, "faithfulness": 1, "critique": "terrible"}""", + _ => "{}" + }; + return Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, json))); + } + }; + + var collection = new TestVectorStoreCollection(); + var orchestrator = new SelfRagOrchestrator(client) { MaxRetries = 0 }; + var result = await orchestrator.GenerateAsync( + collection, "test", + r => (string)r["content"]!); + + Assert.Equal(3, callCount); // No retry calls + Assert.Equal(1.0, result.AverageScore); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/SpeculativeRagOrchestratorTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/SpeculativeRagOrchestratorTests.cs new file mode 100644 index 0000000..602e7c9 --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/SpeculativeRagOrchestratorTests.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.DataRetrieval; +using CommunityToolkit.DataRetrieval.UnitTests.Utils; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class SpeculativeRagOrchestratorTests +{ + [Fact] + public void Constructor_ThrowsOnNullDrafterClient() + { + using var verifier = TestChatClient.WithJsonResponse("{}"); + Assert.Throws("drafterClient", + () => new SpeculativeRagOrchestrator(null!, verifier)); + } + + [Fact] + public void Constructor_ThrowsOnNullVerifierClient() + { + using var drafter = TestChatClient.WithJsonResponse("{}"); + Assert.Throws("verifierClient", + () => new SpeculativeRagOrchestrator(drafter, null!)); + } + + [Fact] + public async Task GenerateAsync_ProducesDraftsInParallel() + { + using var drafter = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, + "Draft answer about the topic."))) + }; + using var verifier = TestChatClient.WithJsonResponse( + """{"bestDraftIndex": 1, "confidence": 0.9, "reasoning": "draft 1 is best"}"""); + + var collection = new TestVectorStoreCollection(); + collection.AddSearchResult("chunk1", 0.9); + collection.AddSearchResult("chunk2", 0.8); + + var orchestrator = new SpeculativeRagOrchestrator(drafter, verifier) + { + DraftCount = 3 + }; + var result = await orchestrator.GenerateAsync( + collection, "test query", + r => (string)r["content"]!); + + Assert.Equal(3, result.Drafts.Count); + Assert.NotEmpty(result.Answer); + Assert.True(result.ParallelDraftMs >= 0); + } + + [Fact] + public async Task GenerateAsync_SelectsBestDraft() + { + int draftIndex = 0; + using var drafter = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + { + var idx = Interlocked.Increment(ref draftIndex); + return Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, + $"Draft {idx} answer."))); + } + }; + using var verifier = TestChatClient.WithJsonResponse( + """{"bestDraftIndex": 2, "confidence": 0.85, "reasoning": "draft 2 most accurate"}"""); + + var collection = new TestVectorStoreCollection(); + collection.AddSearchResult("context", 0.9); + + var orchestrator = new SpeculativeRagOrchestrator(drafter, verifier) { DraftCount = 3 }; + var result = await orchestrator.GenerateAsync( + collection, "query", + r => (string)r["content"]!); + + Assert.Equal(2, result.BestDraftIndex); + Assert.Equal(0.85, result.Confidence); + } + + [Fact] + public async Task GenerateAsync_VerificationFails_FallsBackToFirstDraft() + { + using var drafter = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, + "Fallback draft."))) + }; + using var verifier = TestChatClient.WithException(new InvalidOperationException("verification failed")); + + var collection = new TestVectorStoreCollection(); + collection.AddSearchResult("context", 0.9); + + var orchestrator = new SpeculativeRagOrchestrator(drafter, verifier) { DraftCount = 2 }; + var result = await orchestrator.GenerateAsync( + collection, "query", + r => (string)r["content"]!); + + Assert.Equal(1, result.BestDraftIndex); + Assert.Equal(0.5, result.Confidence); + Assert.Equal("verification failed", result.VerificationReasoning); + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Retrieval/TreeSearchRetrieverTests.cs b/MEDI/test/DataRetrieval.UnitTests/Retrieval/TreeSearchRetrieverTests.cs new file mode 100644 index 0000000..2c802bc --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Retrieval/TreeSearchRetrieverTests.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DataRetrieval; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Retrieval; + +public class TreeSearchRetrieverTests +{ + [Fact] + public async Task ProcessAsync_SetsTreeTraversalMetadata() + { + var retriever = new TreeSearchRetriever(); + var query = new RetrievalQuery("broad question about architecture"); + + var output = await retriever.ProcessAsync(query); + + Assert.Equal("TreeTraversal", output.Metadata["search_paradigm"]); + } + + [Fact] + public async Task ProcessAsync_SetsResultsPerLevel() + { + var retriever = new TreeSearchRetriever { ResultsPerLevel = 5 }; + var query = new RetrievalQuery("test"); + + var output = await retriever.ProcessAsync(query); + + Assert.Equal(5, output.Metadata["results_per_level"]); + } + + [Fact] + public async Task TraverseAsync_GroupsByLevelAndReturnsOrdered() + { + var collection = new Utils.TestVectorStoreCollection(); + collection.AddSearchResult("root summary", 0.7, + new Dictionary { ["level"] = 2, ["id"] = "root", ["parent_id"] = "" }); + collection.AddSearchResult("branch summary", 0.8, + new Dictionary { ["level"] = 1, ["id"] = "branch-1", ["parent_id"] = "root" }); + collection.AddSearchResult("leaf content", 0.9, + new Dictionary { ["level"] = 0, ["id"] = "leaf-1", ["parent_id"] = "branch-1" }); + + var results = await TreeSearchRetriever.TraverseAsync( + collection, + "test query", + contentSelector: r => (string)r["content"]!, + levelSelector: r => (int)r["level"]!, + parentIdSelector: r => r["parent_id"] as string, + idSelector: r => r["id"] as string, + topK: 5); + + Assert.True(results.Count > 0); + // Results should be ordered by score descending + for (int i = 1; i < results.Count; i++) + { + Assert.True(results[i - 1].Score >= results[i].Score); + } + } +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Utils/TestChatClient.cs b/MEDI/test/DataRetrieval.UnitTests/Utils/TestChatClient.cs new file mode 100644 index 0000000..5cde1ae --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Utils/TestChatClient.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.AI; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Utils; + +/// +/// Callback-based fake for unit testing. +/// +public sealed class TestChatClient : IChatClient +{ + public IServiceProvider? Services { get; set; } + + public Func, ChatOptions?, CancellationToken, Task>? + GetResponseAsyncCallback { get; set; } + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + => GetResponseAsyncCallback?.Invoke(messages, options, cancellationToken) + ?? Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "default"))); + + public IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + => throw new NotImplementedException("Streaming not used in tests."); + + public object? GetService(Type serviceType, object? serviceKey = null) + => serviceType is not null && serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null; + + public void Dispose() { } + + /// Creates a TestChatClient that returns the given JSON string as response text. + public static TestChatClient WithJsonResponse(string json) => new() + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, json))) + }; + + /// Creates a TestChatClient that throws the given exception. + public static TestChatClient WithException(Exception ex) => new() + { + GetResponseAsyncCallback = (_, _, _) => throw ex + }; +} diff --git a/MEDI/test/DataRetrieval.UnitTests/Utils/TestVectorStoreCollection.cs b/MEDI/test/DataRetrieval.UnitTests/Utils/TestVectorStoreCollection.cs new file mode 100644 index 0000000..a59fec7 --- /dev/null +++ b/MEDI/test/DataRetrieval.UnitTests/Utils/TestVectorStoreCollection.cs @@ -0,0 +1,90 @@ +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.VectorData; + +namespace CommunityToolkit.DataRetrieval.UnitTests.Utils; + +/// +/// In-memory fake for testing +/// components that depend on vector search. +/// +public sealed class TestVectorStoreCollection : VectorStoreCollection> +{ + private readonly List>> _searchResults = []; + + public override string Name => "test-collection"; + + /// Configures search results to return. + public void SetSearchResults(params VectorSearchResult>[] results) + { + _searchResults.Clear(); + _searchResults.AddRange(results); + } + + /// Adds a search result with the given content and score. + public void AddSearchResult(string content, double score, Dictionary? extraProps = null) + { + var record = new Dictionary + { + ["content"] = content, + ["score"] = score + }; + + if (extraProps is not null) + { + foreach (var kv in extraProps) + record[kv.Key] = kv.Value; + } + + _searchResults.Add(new(record, score)); + } + + public override async IAsyncEnumerable>> SearchAsync( + TInput query, + int top = 5, + VectorSearchOptions>? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + foreach (var result in _searchResults.Take(top)) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return result; + } + } + + public override Task CollectionExistsAsync(CancellationToken cancellationToken = default) + => Task.FromResult(true); + + public override Task EnsureCollectionExistsAsync(CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public override Task EnsureCollectionDeletedAsync(CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public override Task?> GetAsync( + string key, + RecordRetrievalOptions? options = null, + CancellationToken cancellationToken = default) + => Task.FromResult?>(null); + + public override Task DeleteAsync(string key, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public override Task UpsertAsync(Dictionary record, CancellationToken cancellationToken = default) + => Task.FromResult("test-key"); + + public override Task> UpsertAsync( + IEnumerable> records, + CancellationToken cancellationToken = default) + => Task.FromResult>(["test-key"]); + + public override IAsyncEnumerable> GetAsync( + Expression, bool>> filter, + int top = 100, + FilteredRecordRetrievalOptions>? options = null, + CancellationToken cancellationToken = default) + => AsyncEnumerable.Empty>(); + + public override object? GetService(Type serviceType, object? serviceKey = null) => null; +} From b76f64d3c570da491b9ba6d01bd34f88cb5d3b0a Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Tue, 5 May 2026 12:25:22 -0400 Subject: [PATCH 6/7] Add DataIngestion unit tests (14 tests, all passing) Port ingestion processor unit tests covering: EntityExtraction, TopicClassification, HypotheticalQuery, TreeIndex processors. All tests use TestChatClient for deterministic LLM-free execution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DataIngestion.UnitTests.csproj | 34 ++++++++ .../EntityExtractionProcessorTests.cs | 69 +++++++++++++++ .../HypotheticalQueryProcessorTests.cs | 64 ++++++++++++++ .../TopicClassificationProcessorTests.cs | 83 +++++++++++++++++++ .../Ingestion/TreeIndexProcessorTests.cs | 82 ++++++++++++++++++ .../Utils/TestChatClient.cs | 50 +++++++++++ 6 files changed, 382 insertions(+) create mode 100644 MEDI/test/DataIngestion.UnitTests/DataIngestion.UnitTests.csproj create mode 100644 MEDI/test/DataIngestion.UnitTests/Ingestion/EntityExtractionProcessorTests.cs create mode 100644 MEDI/test/DataIngestion.UnitTests/Ingestion/HypotheticalQueryProcessorTests.cs create mode 100644 MEDI/test/DataIngestion.UnitTests/Ingestion/TopicClassificationProcessorTests.cs create mode 100644 MEDI/test/DataIngestion.UnitTests/Ingestion/TreeIndexProcessorTests.cs create mode 100644 MEDI/test/DataIngestion.UnitTests/Utils/TestChatClient.cs diff --git a/MEDI/test/DataIngestion.UnitTests/DataIngestion.UnitTests.csproj b/MEDI/test/DataIngestion.UnitTests/DataIngestion.UnitTests.csproj new file mode 100644 index 0000000..26cc3c7 --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/DataIngestion.UnitTests.csproj @@ -0,0 +1,34 @@ + + + + CommunityToolkit.DataIngestion.UnitTests + CommunityToolkit.DataIngestion.UnitTests + net10.0 + true + false + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/MEDI/test/DataIngestion.UnitTests/Ingestion/EntityExtractionProcessorTests.cs b/MEDI/test/DataIngestion.UnitTests/Ingestion/EntityExtractionProcessorTests.cs new file mode 100644 index 0000000..a303ab5 --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/Ingestion/EntityExtractionProcessorTests.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DataIngestion.UnitTests.Utils; + +namespace CommunityToolkit.DataIngestion.UnitTests.Ingestion; + +public class EntityExtractionProcessorTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", + () => new EntityExtractionProcessor(null!)); + } + + [Fact] + public async Task ProcessAsync_ExtractsEntitiesIntoMetadata() + { + using var client = TestChatClient.WithJsonResponse(""" + { + "people": ["John Doe", "Jane Smith"], + "organizations": ["Microsoft", "OpenAI"], + "technologies": [".NET", "Azure"], + "versions": ["8.0", "10.0"] + } + """); + var processor = new EntityExtractionProcessor(client); + var chunks = CreateChunks("John Doe from Microsoft wrote about .NET 8.0"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.Equal("John Doe, Jane Smith", results[0].Metadata[MetadataKeys.EntitiesPeople]); + Assert.Equal("Microsoft, OpenAI", results[0].Metadata[MetadataKeys.EntitiesOrganizations]); + Assert.Equal(".NET, Azure", results[0].Metadata[MetadataKeys.EntitiesTechnologies]); + Assert.Equal("8.0, 10.0", results[0].Metadata[MetadataKeys.EntitiesVersions]); + } + + [Fact] + public async Task ProcessAsync_LlmThrows_SetsEmptyDefaults() + { + using var client = TestChatClient.WithException(new InvalidOperationException("timeout")); + var processor = new EntityExtractionProcessor(client); + var chunks = CreateChunks("some text"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.Equal("", results[0].Metadata[MetadataKeys.EntitiesPeople]); + Assert.Equal("", results[0].Metadata[MetadataKeys.EntitiesOrganizations]); + Assert.Equal("", results[0].Metadata[MetadataKeys.EntitiesTechnologies]); + Assert.Equal("", results[0].Metadata[MetadataKeys.EntitiesVersions]); + } + + private static async IAsyncEnumerable> CreateChunks(params string[] contents) + { + await Task.CompletedTask; + var doc = new IngestionDocument("test-doc"); + foreach (var content in contents) + yield return new IngestionChunk(content, doc); + } + + private static async Task>> CollectAsync(IAsyncEnumerable> source) + { + var list = new List>(); + await foreach (var item in source) + list.Add(item); + return list; + } +} diff --git a/MEDI/test/DataIngestion.UnitTests/Ingestion/HypotheticalQueryProcessorTests.cs b/MEDI/test/DataIngestion.UnitTests/Ingestion/HypotheticalQueryProcessorTests.cs new file mode 100644 index 0000000..ddab2bc --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/Ingestion/HypotheticalQueryProcessorTests.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DataIngestion.UnitTests.Utils; + +namespace CommunityToolkit.DataIngestion.UnitTests.Ingestion; + +public class HypotheticalQueryProcessorTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", + () => new HypotheticalQueryProcessor(null!)); + } + + [Fact] + public async Task ProcessAsync_GeneratesQuestionsAndYieldsOriginalFirst() + { + using var client = TestChatClient.WithJsonResponse( + """{"questions": ["What is dependency injection?", "How to register services?", "What is IServiceProvider?"]}"""); + var processor = new HypotheticalQueryProcessor(client, questionsPerChunk: 3); + var chunks = CreateChunks("DI in .NET uses IServiceCollection."); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + // Original chunk first, then hypothetical query chunks + Assert.True(results.Count >= 2); + Assert.Equal(MetadataKeys.ChunkTypeOriginal, results[0].Metadata[MetadataKeys.ChunkType]); + + var queryChunks = results.Where(r => + (string)r.Metadata[MetadataKeys.ChunkType]! == MetadataKeys.ChunkTypeHypotheticalQuery).ToList(); + Assert.True(queryChunks.Count > 0); + Assert.All(queryChunks, q => Assert.NotEmpty(q.Content)); + } + + [Fact] + public async Task ProcessAsync_LlmThrows_YieldsOriginalOnly() + { + using var client = TestChatClient.WithException(new InvalidOperationException("timeout")); + var processor = new HypotheticalQueryProcessor(client); + var chunks = CreateChunks("some content"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.Equal(MetadataKeys.ChunkTypeOriginal, results[0].Metadata[MetadataKeys.ChunkType]); + Assert.Equal("some content", results[0].Content); + } + + private static async IAsyncEnumerable> CreateChunks(params string[] contents) + { + await Task.CompletedTask; + var doc = new IngestionDocument("test-doc"); + foreach (var content in contents) + yield return new IngestionChunk(content, doc); + } + + private static async Task>> CollectAsync(IAsyncEnumerable> source) + { + var list = new List>(); + await foreach (var item in source) + list.Add(item); + return list; + } +} diff --git a/MEDI/test/DataIngestion.UnitTests/Ingestion/TopicClassificationProcessorTests.cs b/MEDI/test/DataIngestion.UnitTests/Ingestion/TopicClassificationProcessorTests.cs new file mode 100644 index 0000000..3ef6b27 --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/Ingestion/TopicClassificationProcessorTests.cs @@ -0,0 +1,83 @@ +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DataIngestion.UnitTests.Utils; + +namespace CommunityToolkit.DataIngestion.UnitTests.Ingestion; + +public class TopicClassificationProcessorTests +{ + private static readonly string[] Taxonomy = ["web", "data", "security", "performance", "architecture"]; + + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", + () => new TopicClassificationProcessor(null!, Taxonomy)); + } + + [Fact] + public void Constructor_ThrowsOnNullTaxonomy() + { + using var client = TestChatClient.WithJsonResponse("{}"); + Assert.Throws("taxonomy", + () => new TopicClassificationProcessor(client, null!)); + } + + [Fact] + public async Task ProcessAsync_ValidTopic_SetsMetadata() + { + using var client = TestChatClient.WithJsonResponse( + """{"primary": "security", "secondary": ["architecture", "web"]}"""); + var processor = new TopicClassificationProcessor(client, Taxonomy); + var chunks = CreateChunks("OAuth2 tokens need proper architecture"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.Equal("security", results[0].Metadata[MetadataKeys.TopicPrimary]); + Assert.Contains("architecture", (string)results[0].Metadata[MetadataKeys.TopicSecondary]!); + } + + [Fact] + public async Task ProcessAsync_InvalidPrimary_DefaultsToUnknown() + { + using var client = TestChatClient.WithJsonResponse( + """{"primary": "not_in_taxonomy", "secondary": []}"""); + var processor = new TopicClassificationProcessor(client, Taxonomy); + var chunks = CreateChunks("some random text"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.Equal("unknown", results[0].Metadata[MetadataKeys.TopicPrimary]); + } + + [Fact] + public async Task ProcessAsync_LlmThrows_DefaultsToUnknown() + { + using var client = TestChatClient.WithException(new InvalidOperationException("fail")); + var processor = new TopicClassificationProcessor(client, Taxonomy); + var chunks = CreateChunks("text"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.Equal("unknown", results[0].Metadata[MetadataKeys.TopicPrimary]); + Assert.Equal("", results[0].Metadata[MetadataKeys.TopicSecondary]); + } + + private static async IAsyncEnumerable> CreateChunks(params string[] contents) + { + await Task.CompletedTask; + var doc = new IngestionDocument("test-doc"); + foreach (var content in contents) + yield return new IngestionChunk(content, doc); + } + + private static async Task>> CollectAsync(IAsyncEnumerable> source) + { + var list = new List>(); + await foreach (var item in source) + list.Add(item); + return list; + } +} diff --git a/MEDI/test/DataIngestion.UnitTests/Ingestion/TreeIndexProcessorTests.cs b/MEDI/test/DataIngestion.UnitTests/Ingestion/TreeIndexProcessorTests.cs new file mode 100644 index 0000000..1215602 --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/Ingestion/TreeIndexProcessorTests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DataIngestion.UnitTests.Utils; + +namespace CommunityToolkit.DataIngestion.UnitTests.Ingestion; + +public class TreeIndexProcessorTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", + () => new TreeIndexProcessor(null!)); + } + + [Fact] + public async Task ProcessAsync_MarksLeafChunksWithLevel0() + { + using var client = TestChatClient.WithJsonResponse("Branch summary text"); + var processor = new TreeIndexProcessor(client); + var chunks = CreateChunks("Chunk 1 content", "Chunk 2 content"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + var leaves = results.Where(r => (int)r.Metadata[MetadataKeys.Level]! == 0).ToList(); + Assert.Equal(2, leaves.Count); + Assert.All(leaves, l => Assert.Equal(MetadataKeys.ChunkTypeOriginal, l.Metadata[MetadataKeys.ChunkType])); + } + + [Fact] + public async Task ProcessAsync_GeneratesBranchAndRootSummaries() + { + int callCount = 0; + using var client = new TestChatClient + { + GetResponseAsyncCallback = (_, _, _) => + { + callCount++; + var text = callCount == 1 + ? "Branch summary of the document." + : "Root summary of the entire corpus."; + return Task.FromResult(new Microsoft.Extensions.AI.ChatResponse( + new Microsoft.Extensions.AI.ChatMessage( + Microsoft.Extensions.AI.ChatRole.Assistant, text))); + } + }; + var processor = new TreeIndexProcessor(client); + var chunks = CreateChunks("Chunk 1", "Chunk 2", "Chunk 3"); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + // Should have leaf chunks + at least 1 branch + 1 root summary + var branches = results.Where(r => + r.Metadata.ContainsKey(MetadataKeys.ChunkType) && + (string)r.Metadata[MetadataKeys.ChunkType]! == MetadataKeys.ChunkTypeBranchSummary).ToList(); + var roots = results.Where(r => + r.Metadata.ContainsKey(MetadataKeys.ChunkType) && + (string)r.Metadata[MetadataKeys.ChunkType]! == MetadataKeys.ChunkTypeRootSummary).ToList(); + + Assert.True(branches.Count >= 1, "Should generate at least one branch summary"); + Assert.True(roots.Count >= 1, "Should generate at least one root summary"); + + // Branch = level 1, Root = level 2 + Assert.All(branches, b => Assert.Equal(1, b.Metadata[MetadataKeys.Level])); + Assert.All(roots, r => Assert.Equal(2, r.Metadata[MetadataKeys.Level])); + } + + private static async IAsyncEnumerable> CreateChunks(params string[] contents) + { + await Task.CompletedTask; + var doc = new IngestionDocument("test-doc"); + foreach (var content in contents) + yield return new IngestionChunk(content, doc); + } + + private static async Task>> CollectAsync(IAsyncEnumerable> source) + { + var list = new List>(); + await foreach (var item in source) + list.Add(item); + return list; + } +} diff --git a/MEDI/test/DataIngestion.UnitTests/Utils/TestChatClient.cs b/MEDI/test/DataIngestion.UnitTests/Utils/TestChatClient.cs new file mode 100644 index 0000000..5e151f4 --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/Utils/TestChatClient.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.AI; + +namespace CommunityToolkit.DataIngestion.UnitTests.Utils; + +/// +/// A fake IChatClient for deterministic unit testing (no real LLM calls). +/// +public sealed class TestChatClient : IChatClient, IDisposable +{ + public Func, ChatOptions?, CancellationToken, Task>? + GetResponseAsyncCallback { get; set; } + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + if (GetResponseAsyncCallback is not null) + return GetResponseAsyncCallback(messages, options, cancellationToken); + + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, ""))); + } + + public IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() { } + + /// Creates a TestChatClient that always returns the given JSON string. + public static TestChatClient WithJsonResponse(string json) + => new() + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, json))) + }; + + /// Creates a TestChatClient that always throws the given exception. + public static TestChatClient WithException(Exception ex) + => new() + { + GetResponseAsyncCallback = (_, _, _) => throw ex + }; +} From 9420997a7e8c5a433270ca775912d471826da3d9 Mon Sep 17 00:00:00 2001 From: luisquintanilla Date: Tue, 30 Jun 2026 23:17:34 -0400 Subject: [PATCH 7/7] Add ContextualChunkEnricher to DataIngestion --- Directory.Packages.props | 4 +- .../DataIngestion/ContextualChunkEnricher.cs | 52 ++++++ MEDI/src/DataIngestion/DataIngestion.csproj | 2 +- ...ataIngestionServiceCollectionExtensions.cs | 1 + .../DataIngestion/IngestionPipelineBuilder.cs | 9 + MEDI/src/DataIngestion/MetadataKeys.cs | 3 + .../Ingestion/ContextualChunkEnricherTests.cs | 157 ++++++++++++++++++ 7 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 MEDI/src/DataIngestion/ContextualChunkEnricher.cs create mode 100644 MEDI/test/DataIngestion.UnitTests/Ingestion/ContextualChunkEnricherTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 768fdb1..4e4afed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,8 +23,8 @@ - - + + diff --git a/MEDI/src/DataIngestion/ContextualChunkEnricher.cs b/MEDI/src/DataIngestion/ContextualChunkEnricher.cs new file mode 100644 index 0000000..ec1cc9f --- /dev/null +++ b/MEDI/src/DataIngestion/ContextualChunkEnricher.cs @@ -0,0 +1,52 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; + +namespace CommunityToolkit.DataIngestion; + +/// +/// An that enriches text chunks with contextual +/// summaries for improved RAG retrieval. +/// +public sealed class ContextualChunkEnricher : IngestionChunkProcessor +{ + private readonly IChatClient _chatClient; + + /// + /// Initializes a new instance of the class. + /// + /// The chat client used to generate contextual summaries. + public ContextualChunkEnricher(IChatClient chatClient) + { + _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); + } + + /// + public override async IAsyncEnumerable> ProcessAsync( + IAsyncEnumerable> chunks, + [EnumeratorCancellation] CancellationToken ct = default) + { + await foreach (var chunk in chunks.WithCancellation(ct)) + { + var prompt = GetPromptForChunk(chunk); + var messages = new[] { new ChatMessage(ChatRole.User, prompt) }; + var response = await _chatClient.GetResponseAsync(messages, cancellationToken: ct); + + chunk.Metadata[MetadataKeys.ContextualSummary] = response.Text; + yield return chunk; + } + } + + private static string GetPromptForChunk(IngestionChunk chunk) + { + var elementType = chunk.Metadata.TryGetValue("element_type", out var t) ? t as string : null; + var instruction = elementType switch + { + "table" => "Summarize this table for search retrieval. Describe what data it contains, including key metrics, column headers, and notable values. Output only the summary sentence, nothing else.", + "picture" or "caption" => "Summarize what this figure or image refers to for search retrieval. Output only the summary sentence, nothing else.", + _ => "Provide a single concise sentence summarizing the following text for use in search retrieval. Output only the summary sentence, nothing else." + }; + + return instruction + "\n\n" + chunk.Content; + } +} diff --git a/MEDI/src/DataIngestion/DataIngestion.csproj b/MEDI/src/DataIngestion/DataIngestion.csproj index 8b70edd..698edd1 100644 --- a/MEDI/src/DataIngestion/DataIngestion.csproj +++ b/MEDI/src/DataIngestion/DataIngestion.csproj @@ -6,7 +6,7 @@ net10.0;net8.0 LLM-powered chunk processors for Microsoft.Extensions.DataIngestion pipelines - LLM-powered ingestion chunk processors including entity extraction, topic classification, hypothetical query generation (reverse HyDE), and RAPTOR-style tree indexing for advanced RAG applications. + LLM-powered ingestion chunk processors including contextual chunk enrichment, entity extraction, topic classification, hypothetical query generation (reverse HyDE), and RAPTOR-style tree indexing for advanced RAG applications. true diff --git a/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs b/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs index 782ba5b..847f86e 100644 --- a/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs +++ b/MEDI/src/DataIngestion/DataIngestionServiceCollectionExtensions.cs @@ -18,6 +18,7 @@ public static class DataIngestionServiceCollectionExtensions /// .UseEntityExtraction() /// .UseTopicClassification(o => o.Taxonomy = ["web", "data", "security"]) /// .UseHypotheticalQueries(o => o.QuestionsPerChunk = 5) + /// .UseContextualChunkEnrichment() /// .UseTreeIndex(); /// /// diff --git a/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs b/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs index 72baf43..b297b56 100644 --- a/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs +++ b/MEDI/src/DataIngestion/IngestionPipelineBuilder.cs @@ -92,6 +92,15 @@ public IngestionPipelineBuilder UseHypotheticalQueries( return this; } + /// Adds contextual chunk enrichment for improved retrieval. + public IngestionPipelineBuilder UseContextualChunkEnrichment() + { + ChunkProcessorFactories.Add(sp => + (IngestionChunkProcessor)(object)new ContextualChunkEnricher( + sp.GetRequiredService())); + return this; + } + /// Adds RAPTOR-style tree index generation (leaf → branch → root summaries). public IngestionPipelineBuilder UseTreeIndex() { diff --git a/MEDI/src/DataIngestion/MetadataKeys.cs b/MEDI/src/DataIngestion/MetadataKeys.cs index 9b992ee..d862857 100644 --- a/MEDI/src/DataIngestion/MetadataKeys.cs +++ b/MEDI/src/DataIngestion/MetadataKeys.cs @@ -32,6 +32,9 @@ public static class MetadataKeys /// Pipe-delimited hypothetical questions generated for a chunk. public const string HypotheticalQuestions = "hypothetical_questions"; + /// Contextual summary generated for a chunk. + public const string ContextualSummary = "contextual_summary"; + /// Tree hierarchy level (0 = leaf, 1 = branch, 2 = root). public const string Level = "level"; diff --git a/MEDI/test/DataIngestion.UnitTests/Ingestion/ContextualChunkEnricherTests.cs b/MEDI/test/DataIngestion.UnitTests/Ingestion/ContextualChunkEnricherTests.cs new file mode 100644 index 0000000..8bb6180 --- /dev/null +++ b/MEDI/test/DataIngestion.UnitTests/Ingestion/ContextualChunkEnricherTests.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using CommunityToolkit.DataIngestion.UnitTests.Utils; + +namespace CommunityToolkit.DataIngestion.UnitTests.Ingestion; + +public class ContextualChunkEnricherTests +{ + [Fact] + public void Constructor_ThrowsOnNullChatClient() + { + Assert.Throws("chatClient", + () => new ContextualChunkEnricher(null!)); + } + + [Fact] + public async Task ProcessAsync_AddsContextualSummaryMetadata() + { + var summaryText = "This chunk discusses PDF text extraction."; + using var client = CreateClientReturning(summaryText); + var processor = new ContextualChunkEnricher(client); + var chunks = CreateChunks("Some text about PDF extraction and analysis."); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Single(results); + Assert.True(results[0].Metadata.ContainsKey(MetadataKeys.ContextualSummary)); + Assert.Equal(summaryText, results[0].Metadata[MetadataKeys.ContextualSummary]); + } + + [Fact] + public async Task ProcessAsync_MultipleChunks_AllGetSummaries() + { + var summaryText = "Summary text."; + using var client = CreateClientReturning(summaryText); + var processor = new ContextualChunkEnricher(client); + var chunks = CreateChunks("First chunk content.", "Second chunk content."); + + var results = await CollectAsync(processor.ProcessAsync(chunks)); + + Assert.Equal(2, results.Count); + Assert.All(results, r => + { + Assert.True(r.Metadata.ContainsKey(MetadataKeys.ContextualSummary)); + Assert.Equal(summaryText, r.Metadata[MetadataKeys.ContextualSummary]); + }); + } + + [Fact] + public async Task ProcessAsync_EmptyChunks_NoResults() + { + using var client = CreateClientReturning("summary"); + var processor = new ContextualChunkEnricher(client); + + var results = await CollectAsync(processor.ProcessAsync(CreateChunks())); + + Assert.Empty(results); + } + + [Fact] + public async Task ProcessAsync_CancellationRequested_Throws() + { + using var client = new TestChatClient + { + GetResponseAsyncCallback = (_, _, ct) => + { + ct.ThrowIfCancellationRequested(); + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "summary"))); + } + }; + var processor = new ContextualChunkEnricher(client); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var _ in processor.ProcessAsync(CreateChunks("Some content."), cts.Token)) + { + } + }); + } + + [Fact] + public async Task ProcessAsync_PreservesChunkContentDocumentAndInstance() + { + using var client = CreateClientReturning("A summary."); + var processor = new ContextualChunkEnricher(client); + var document = new IngestionDocument("test-doc"); + var originalContent = "Original chunk content stays the same."; + var chunk = new IngestionChunk(originalContent, document); + + var results = await CollectAsync(processor.ProcessAsync(CreateChunkInstances(chunk))); + + var result = Assert.Single(results); + Assert.Same(chunk, result); + Assert.Same(document, result.Document); + Assert.Equal(originalContent, result.Content); + } + + [Theory] + [InlineData("table", "Summarize this table for search retrieval. Describe what data it contains, including key metrics, column headers, and notable values. Output only the summary sentence, nothing else.")] + [InlineData("picture", "Summarize what this figure or image refers to for search retrieval. Output only the summary sentence, nothing else.")] + [InlineData("caption", "Summarize what this figure or image refers to for search retrieval. Output only the summary sentence, nothing else.")] + [InlineData(null, "Provide a single concise sentence summarizing the following text for use in search retrieval. Output only the summary sentence, nothing else.")] + public async Task ProcessAsync_UsesElementTypeSpecificPrompt(string? elementType, string expectedInstruction) + { + string? prompt = null; + using var client = new TestChatClient + { + GetResponseAsyncCallback = (messages, _, _) => + { + prompt = Assert.Single(messages).Text; + return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "summary"))); + } + }; + var processor = new ContextualChunkEnricher(client); + var document = new IngestionDocument("test-doc"); + var chunk = new IngestionChunk("Chunk content.", document); + if (elementType is not null) + chunk.Metadata["element_type"] = elementType; + + await CollectAsync(processor.ProcessAsync(CreateChunkInstances(chunk))); + + Assert.Equal(expectedInstruction + "\n\nChunk content.", prompt); + } + + private static TestChatClient CreateClientReturning(string response) + => new() + { + GetResponseAsyncCallback = (_, _, _) => + Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, response))) + }; + + private static async IAsyncEnumerable> CreateChunks(params string[] contents) + { + await Task.CompletedTask; + var doc = new IngestionDocument("test-doc"); + foreach (var content in contents) + yield return new IngestionChunk(content, doc); + } + + private static async IAsyncEnumerable> CreateChunkInstances(params IngestionChunk[] chunks) + { + await Task.CompletedTask; + foreach (var chunk in chunks) + yield return chunk; + } + + private static async Task>> CollectAsync(IAsyncEnumerable> source) + { + var list = new List>(); + await foreach (var item in source) + list.Add(item); + return list; + } +}