Skip to content

Commit 7bd388a

Browse files
committed
Configure FileContext PDF and image content through options
1 parent 511652e commit 7bd388a

17 files changed

Lines changed: 309 additions & 56 deletions

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Tests use xUnit over VSTest. NuGet versions are centrally managed in `Directory.
3939

4040
## Boundaries
4141

42+
- Expose FileContext behavior and limits through typed options registered with the standard `IOptions<T>` pattern; hosts bind configuration and consumers use those resolved options rather than introducing fixed capacity constants.
43+
- Expose image content as binary bytes, base64, and URL references through typed package APIs so hosts can select the correct model-visible representation.
4244
- `ManagedCode.Storage.Core.IStorage` is the only storage contract the product package may require.
4345
- Do not depend on a concrete storage provider in product code.
4446
- Keep Microsoft Agent Framework adaptation separate from Markdown graph materialization.

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to ManagedCode.FileContext are documented here.
44

5+
## 1.0.11
6+
7+
- Bind and validate FileContext limits through `IOptions<FileContextOptions>`, including keyed registrations and host configuration.
8+
- Apply configured PDF source, page, and image limits to direct rendering and storage-backed tools.
9+
- Add typed model image content helpers for PNG bytes, base64, and HTTPS URL references.
10+
511
## 1.0.10
612

713
- Raise the bounded PDF source read limit from 25 MiB to 100 MiB for scanned documents while retaining per-page pixel and PNG output limits.

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<AnalysisMode>Recommended</AnalysisMode>
1313
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1414
<NoWarn>$(NoWarn);CS1591;MAAI001</NoWarn>
15-
<Version>1.0.10</Version>
15+
<Version>1.0.11</Version>
1616
<PackageVersion>$(Version)</PackageVersion>
1717
</PropertyGroup>
1818

‎Directory.Packages.props‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.12" />
3131
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.12" />
3232
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.10.1" />
33-
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.11" />
34-
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.11" />
33+
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.12" />
34+
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.12" />
3535
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
3636
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" />
3737
<PackageVersion Include="OpenAI" Version="2.14.0" />

‎README.md‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ Results include `StartLine`, `EndLine`, `HasMore`, and `TotalLines` when the end
187187

188188
`IFileContextPdf.ReadPdfTextAsync(path)` returns bounded text, `PageCount`, and one-based `PagesWithoutText`. It does not perform OCR. A scanned page can instead be rendered with `RenderPdfPageAsync(path, pageNumber)`, which returns PNG `DataContent`. Use `CountPdfPageImagesAsync` and `ExtractPdfImageAsync` when the original embedded pictures are needed rather than the complete page. The four read-only `file_context_pdf_*` tools expose the same operations from scoped storage.
189189

190-
For an authenticated PDF already held as bytes, `FileContextPdfTextExtractor.Extract`, `FileContextPdfImages.RenderPagePng`, and `FileContextPdfImages.ExtractPageImagesPng` work without storing it. PDF source reads are capped at 100 MiB by default; page rasterization caps pixels and PNG size. A host must pass image `DataContent` to its model as image content. A generic OpenAI Chat function result serializes it as text, so hosts must explicitly bridge image tool results into a multimodal model message.
190+
For an authenticated PDF already held as bytes, `FileContextPdfTextExtractor.Extract`, `FileContextPdfImages.RenderPagePng`, and `FileContextPdfImages.ExtractPageImagesPng` work without storing it. PDF source reads default to 100 MiB and accept `FileContextOptions` for a different limit; page rasterization also uses configured pixel and PNG limits. `FileContextImageContent` creates model-visible `DataContent` from PNG bytes or base64 and `UriContent` from an HTTPS URL. A URL reference is not fetched by FileContext, so the model provider must be able to access it. A host must pass image content to its model as image content. A generic OpenAI Chat function result serializes it as text, so hosts must explicitly bridge image tool results into a multimodal model message.
191191

192192
`file_context_docx_text(path, startParagraph?, startCharacter?, paragraphCount?)` reads ordinary paragraph and table text from a scoped DOCX package. The result contains numbered paragraph segments and `nextParagraph`/`nextCharacter`; use that cursor to continue a long document. Reads are limited to 50 paragraphs and 20,000 characters per call, with a configurable 25 MiB source limit (`MaximumDocxReadBytes`). It does not OCR embedded images. DOCX and XLSX packages are excluded from generic text reads and grep.
193193

@@ -245,6 +245,11 @@ Paths are logical, relative, and `/`-separated. `RootPrefix` scopes storage acce
245245

246246
| Option | Default |
247247
| --- | ---: |
248+
| `MaximumPdfReadBytes` | 100 MiB |
249+
| `MaximumImageBytes` | 8 MiB |
250+
| `MaximumRenderedPagePixels` / `MaximumImagesPerPdfPage` | 4,000,000 / 20 |
251+
| `DefaultPdfPageScale` / `MinimumPdfPageScale` / `MaximumPdfPageScale` | 1.5 / 0.5 / 3 |
252+
| `PdfPngQuality` | 100 |
248253
| `MaximumFullReadBytes` | 1 MiB |
249254
| `MaximumRangeReadBytes` | 256 KiB |
250255
| `DefaultRangeLineCount` / `MaximumRangeLineCount` | 200 / 1,000 |
@@ -263,7 +268,7 @@ These settings are available through `FileContextOptions`; their named defaults
263268

264269
### Configure limits and timeouts
265270

266-
Every option in the table is configurable through `FileContextOptions`, for both default and keyed registrations. Configure the options before building your service provider:
271+
Every option in the table is configurable through `IOptions<FileContextOptions>`, for both default and keyed registrations. Configure the options before building your service provider:
267272

268273
```csharp
269274
services.AddManagedCodeFileContext(storage, options =>
@@ -291,17 +296,27 @@ services.AddManagedCodeFileContext(storage, options =>
291296
In a host that uses Microsoft configuration binding, the same options can come from `appsettings.json`, environment variables, or another configuration source:
292297

293298
```csharp
294-
using Microsoft.Extensions.Configuration;
299+
services.AddFileContextOptions(configuration);
300+
services.AddManagedCodeFileContext(storage);
301+
```
295302

296-
services.AddManagedCodeFileContext(storage, options =>
297-
configuration.GetSection("FileContext").Bind(options));
303+
The package binds the `FileContext` section through the standard options pipeline. For example:
304+
305+
```csharp
306+
var options = provider.GetRequiredService<IOptions<FileContextOptions>>().Value;
307+
var page = FileContextPdfImages.RenderPagePng(pdfBytes, pageNumber, options);
308+
var raw = FileContextImageContent.FromPngBytes(page, options);
309+
var base64 = FileContextImageContent.ToBase64Png(page, options);
310+
var fromBase64 = FileContextImageContent.FromBase64Png(base64, options);
311+
var remote = FileContextImageContent.FromPngUrl(new Uri("https://example.com/page.png"));
298312
```
299313

300-
The host supplies `configuration` and the `Microsoft.Extensions.Configuration.Binder` package. For example:
314+
The URL form is a reference only; FileContext does not download it. Use a URL only when the model provider can fetch that resource.
301315

302316
```json
303317
{
304318
"FileContext": {
319+
"MaximumPdfReadBytes": 104857600,
305320
"MaximumFullReadBytes": 4194304,
306321
"MaximumRangeReadBytes": 524288,
307322
"DefaultRangeLineCount": 100,
@@ -325,7 +340,7 @@ The host supplies `configuration` and the `Microsoft.Extensions.Configuration.Bi
325340

326341
Configured deadline expiry surfaces as `TimeoutException`, which the function-invocation loop can return as a tool error. Caller cancellation remains `OperationCanceledException`. Cancellation is cooperative: a provider that ignores the token or a synchronous regex/graph operation can finish later than the deadline; FileContext awaits the work and checks cancellation before returning a successful result. Regex matching retains its own `RegexTimeout`.
327342

328-
Options are bound at registration time; changing the configuration later does not automatically reconfigure an existing provider. To set a per-call deadline or allow the caller to cancel earlier, pass a cancellation token:
343+
`IOptions<FileContextOptions>` resolves the configured values when the provider is created. Existing providers keep their resolved options. To set a per-call deadline or allow the caller to cancel earlier, pass a cancellation token:
329344

330345
```csharp
331346
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));

‎docs/Architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ flowchart TD
7777

7878
## Operational limits
7979

80-
All potentially large operations are controlled by `FileContextOptions`: full-read bytes, range bytes, files scanned, bytes per searched file, matches per file, total search results, graph documents, graph source bytes, and exported graph characters. Non-seekable cloud streams are supported by sequential streaming.
80+
All potentially large operations are controlled by `IOptions<FileContextOptions>`: PDF source/page/image budgets, full-read bytes, range bytes, files scanned, bytes per searched file, matches per file, total search results, graph documents, graph source bytes, and exported graph characters. Non-seekable cloud streams are supported by sequential streaming.
8181

8282
## Start here
8383

‎src/ManagedCode.FileContext/FileContextDefaults.cs‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
using ManagedCode.FileContext.Pdf;
2-
31
namespace ManagedCode.FileContext;
42

53
/// <summary>Default limits and selectors used by <see cref="FileContextOptions" />.</summary>
64
public static class FileContextDefaults
75
{
6+
public const int MaximumPdfPngQuality = 100;
87
public const long MaximumGeneratedFileBytes = 64L * 1024L * 1024L;
98
public const int FirstLineNumber = 1;
10-
public const int MaximumPdfReadBytes = FileContextPdfImages.MaximumPdfBytes;
9+
public const int MaximumPdfReadBytes = 100 * 1024 * 1024;
10+
public const int MaximumImageBytes = 8 * 1024 * 1024;
11+
public const int MaximumRenderedPagePixels = 4_000_000;
12+
public const int MaximumImagesPerPdfPage = 20;
13+
public const double DefaultPdfPageScale = 1.5;
14+
public const double MinimumPdfPageScale = 0.5;
15+
public const double MaximumPdfPageScale = 3;
16+
public const int PdfPngQuality = 100;
1117
public const int MaximumPdfTextCharacters = 50_000;
1218
public const int MaximumDocxReadBytes = 25 * 1024 * 1024;
1319
public const int MaximumDocxTextCharacters = 20_000;

‎src/ManagedCode.FileContext/FileContextOptions.cs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ namespace ManagedCode.FileContext;
33
/// <summary>Controls file access, approval, search, and graph limits for one context provider.</summary>
44
public sealed class FileContextOptions
55
{
6+
public const string SectionName = "FileContext";
7+
68
private static readonly TimeSpan MaximumOperationTimeout = TimeSpan.FromMilliseconds(uint.MaxValue - 1L);
79
private static readonly TimeSpan MaximumRegexTimeout = TimeSpan.FromMilliseconds(int.MaxValue - 1);
810

@@ -21,6 +23,23 @@ public sealed class FileContextOptions
2123

2224
public int MaximumPdfReadBytes { get; set; } = FileContextDefaults.MaximumPdfReadBytes;
2325

26+
public int MaximumImageBytes { get; set; } = FileContextDefaults.MaximumImageBytes;
27+
28+
public int MaximumRenderedPagePixels { get; set; } = FileContextDefaults.MaximumRenderedPagePixels;
29+
30+
public int MaximumImagesPerPdfPage { get; set; } = FileContextDefaults.MaximumImagesPerPdfPage;
31+
32+
public double DefaultPdfPageScale { get; set; } = FileContextDefaults.DefaultPdfPageScale;
33+
34+
public double MinimumPdfPageScale { get; set; } = FileContextDefaults.MinimumPdfPageScale;
35+
36+
public double MaximumPdfPageScale { get; set; } = FileContextDefaults.MaximumPdfPageScale;
37+
38+
public int PdfPngQuality { get; set; } = FileContextDefaults.PdfPngQuality;
39+
40+
/// <summary>Creates an independent copy for a scoped provider while retaining configured limits.</summary>
41+
public FileContextOptions Clone() => (FileContextOptions)MemberwiseClone();
42+
2443
public int MaximumDocxReadBytes { get; set; } = FileContextDefaults.MaximumDocxReadBytes;
2544

2645
public long MaximumFullReadBytes { get; set; } = FileContextDefaults.MaximumFullReadBytes;
@@ -56,6 +75,19 @@ internal void Validate()
5675
{
5776
ValidatePositive(MaximumGeneratedFileBytes, nameof(MaximumGeneratedFileBytes));
5877
ValidatePositive(MaximumPdfReadBytes, nameof(MaximumPdfReadBytes));
78+
ValidatePositive(MaximumImageBytes, nameof(MaximumImageBytes));
79+
ValidatePositive(MaximumRenderedPagePixels, nameof(MaximumRenderedPagePixels));
80+
ValidatePositive(MaximumImagesPerPdfPage, nameof(MaximumImagesPerPdfPage));
81+
if (!double.IsFinite(MinimumPdfPageScale) || MinimumPdfPageScale <= 0
82+
|| !double.IsFinite(DefaultPdfPageScale) || DefaultPdfPageScale < MinimumPdfPageScale
83+
|| !double.IsFinite(MaximumPdfPageScale) || MaximumPdfPageScale < DefaultPdfPageScale)
84+
{
85+
throw new InvalidOperationException("PDF page scales must be finite, positive, and ordered.");
86+
}
87+
if (PdfPngQuality is < 0 or > FileContextDefaults.MaximumPdfPngQuality)
88+
{
89+
throw new InvalidOperationException("PDF PNG quality must be between 0 and 100.");
90+
}
5991
ValidatePositive(MaximumDocxReadBytes, nameof(MaximumDocxReadBytes));
6092
ValidatePositive(MaximumFullReadBytes, nameof(MaximumFullReadBytes));
6193
ValidatePositive(MaximumRangeReadBytes, nameof(MaximumRangeReadBytes));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Microsoft.Extensions.Options;
2+
3+
namespace ManagedCode.FileContext;
4+
5+
internal sealed class FileContextOptionsValidator : IValidateOptions<FileContextOptions>
6+
{
7+
public ValidateOptionsResult Validate(string? name, FileContextOptions options)
8+
{
9+
try
10+
{
11+
options.Validate();
12+
return ValidateOptionsResult.Success;
13+
}
14+
catch (InvalidOperationException exception)
15+
{
16+
return ValidateOptionsResult.Fail(exception.Message);
17+
}
18+
}
19+
}

‎src/ManagedCode.FileContext/FileContextService.Pdf.cs‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace ManagedCode.FileContext;
66
public sealed partial class FileContextService
77
{
88
private const string PdfExtension = ".pdf";
9-
private const string PngMediaType = "image/png";
109
private const int CopyBufferSize = 81920;
1110

1211
public Task<FileContextPdfText> ReadPdfTextAsync(string path,
@@ -24,14 +23,14 @@ public Task<FileContextPdfText> ReadPdfTextAsync(string path,
2423
public Task<DataContent> RenderPdfPageAsync(string path, int pageNumber,
2524
CancellationToken cancellationToken = default) =>
2625
FileContextOperation.RunAsync(_options.OperationTimeout, async token =>
27-
new DataContent(FileContextPdfImages.RenderPagePng(
28-
await ReadPdfBytesAsync(path, token).ConfigureAwait(false), pageNumber), PngMediaType), cancellationToken);
26+
FileContextImageContent.FromPngBytes(FileContextPdfImages.RenderPagePng(
27+
await ReadPdfBytesAsync(path, token).ConfigureAwait(false), pageNumber, _options), _options), cancellationToken);
2928

3029
public Task<int> CountPdfPageImagesAsync(string path, int pageNumber,
3130
CancellationToken cancellationToken = default) =>
3231
FileContextOperation.RunAsync(_options.OperationTimeout, async token =>
3332
FileContextPdfImages.ExtractPageImagesPng(
34-
await ReadPdfBytesAsync(path, token).ConfigureAwait(false), pageNumber).Count, cancellationToken);
33+
await ReadPdfBytesAsync(path, token).ConfigureAwait(false), pageNumber, _options).Count, cancellationToken);
3534

3635
public Task<DataContent> ExtractPdfImageAsync(string path, int pageNumber, int imageNumber,
3736
CancellationToken cancellationToken = default)
@@ -40,12 +39,12 @@ public Task<DataContent> ExtractPdfImageAsync(string path, int pageNumber, int i
4039
return FileContextOperation.RunAsync(_options.OperationTimeout, async token =>
4140
{
4241
var images = FileContextPdfImages.ExtractPageImagesPng(
43-
await ReadPdfBytesAsync(path, token).ConfigureAwait(false), pageNumber);
42+
await ReadPdfBytesAsync(path, token).ConfigureAwait(false), pageNumber, _options);
4443
if (imageNumber > images.Count)
4544
{
4645
throw new InvalidOperationException("The embedded image number is outside this PDF page.");
4746
}
48-
return new DataContent(images[imageNumber - 1].PngBytes, PngMediaType);
47+
return FileContextImageContent.FromPngBytes(images[imageNumber - 1].PngBytes, _options);
4948
}, cancellationToken);
5049
}
5150

0 commit comments

Comments
 (0)