Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Input/MuPDF.NET.PDF4LLM/Magazines-PDF.pdf
Binary file not shown.
Binary file added Input/MuPDF.NET.PDF4LLM/nato-members.pdf
Binary file not shown.
45 changes: 45 additions & 0 deletions MuPDF.NET.Examples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "30-FileAnnot", "MuPDF.NET\3
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "31-ParallelPixmap", "MuPDF.NET\31-ParallelPixmap\31-ParallelPixmap.csproj", "{6AD9C794-F0D4-4005-A89F-3AA11CF998CF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "09-LoadAi", "MuPDF.NET.PDF4LLM\09-LoadAi\09-LoadAi.csproj", "{6930829A-5FEE-4711-8FFA-483C8FEC719D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "10-Ollama", "MuPDF.NET.PDF4LLM\10-Ollama\10-Ollama.csproj", "{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "11-OllamaMagazines", "MuPDF.NET.PDF4LLM\11-OllamaMagazines\11-OllamaMagazines.csproj", "{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -651,6 +657,42 @@ Global
{6AD9C794-F0D4-4005-A89F-3AA11CF998CF}.Release|x64.Build.0 = Release|Any CPU
{6AD9C794-F0D4-4005-A89F-3AA11CF998CF}.Release|x86.ActiveCfg = Release|Any CPU
{6AD9C794-F0D4-4005-A89F-3AA11CF998CF}.Release|x86.Build.0 = Release|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Debug|x64.ActiveCfg = Debug|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Debug|x64.Build.0 = Debug|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Debug|x86.ActiveCfg = Debug|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Debug|x86.Build.0 = Debug|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Release|Any CPU.Build.0 = Release|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Release|x64.ActiveCfg = Release|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Release|x64.Build.0 = Release|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Release|x86.ActiveCfg = Release|Any CPU
{6930829A-5FEE-4711-8FFA-483C8FEC719D}.Release|x86.Build.0 = Release|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Debug|x64.ActiveCfg = Debug|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Debug|x64.Build.0 = Debug|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Debug|x86.ActiveCfg = Debug|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Debug|x86.Build.0 = Debug|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Release|Any CPU.Build.0 = Release|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Release|x64.ActiveCfg = Release|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Release|x64.Build.0 = Release|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Release|x86.ActiveCfg = Release|Any CPU
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17}.Release|x86.Build.0 = Release|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Debug|x64.ActiveCfg = Debug|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Debug|x64.Build.0 = Debug|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Debug|x86.ActiveCfg = Debug|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Debug|x86.Build.0 = Debug|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Release|Any CPU.Build.0 = Release|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Release|x64.ActiveCfg = Release|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Release|x64.Build.0 = Release|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Release|x86.ActiveCfg = Release|Any CPU
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -701,5 +743,8 @@ Global
{45390D20-78E7-4DB4-A31F-59E5DCC3BDB2} = {C9700E2F-F6F1-A264-19EE-D9EB5C62093D}
{0E41A6C8-E17A-48C2-A7EA-BEEC651F3C00} = {C9700E2F-F6F1-A264-19EE-D9EB5C62093D}
{6AD9C794-F0D4-4005-A89F-3AA11CF998CF} = {C9700E2F-F6F1-A264-19EE-D9EB5C62093D}
{6930829A-5FEE-4711-8FFA-483C8FEC719D} = {EEFA252F-3CE0-C131-3F8C-AF1C5E35DF91}
{B8C1D4E2-7A3F-4B9C-9E51-2D6A8F0C4B17} = {EEFA252F-3CE0-C131-3F8C-AF1C5E35DF91}
{C9D2E5F3-8B40-4CAD-AF62-3E7B9A1D5C28} = {EEFA252F-3CE0-C131-3F8C-AF1C5E35DF91}
EndGlobalSection
EndGlobal
4 changes: 4 additions & 0 deletions MuPDF.NET.PDF4LLM/01-ToMarkdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ dotnet run --project MuPDF.NET.PDF4LLM\01-ToMarkdown

- `MuPDF4LLM.SetUseLayout(false)`
- `MuPDF4LLM.ToMarkdown`

## Related

- [`09-LoadAi`](../09-LoadAi/) — index that Markdown, then Ask / Search (why this package exists)
4 changes: 4 additions & 0 deletions MuPDF.NET.PDF4LLM/06-LlamaMarkdownReader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ dotnet run --project MuPDF.NET.PDF4LLM\06-LlamaMarkdownReader
- `MuPDF4LLM.SetUseLayout(false)` — classic RAG markdown (stable Expected/)
- `PDFMarkdownReader.LoadData`
- `LlamaIndexDocument.Text` / `ExtraInfo`

## Related

- [`09-LoadAi`](../09-LoadAi/) — embed those chunks and Ask / Search
12 changes: 12 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/09-LoadAi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>MuPDF.NET.Examples.PDF4LLM.LoadAi</RootNamespace>
<AssemblyName>09-LoadAi</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MuPDF.NET.PDF4LLM" Version="$(ArtifexMuPDFNetPDF4LLMVersion)" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.3.0" />
<ProjectReference Include="..\..\Common\Examples.Common.csproj" />
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/Expected/ask.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Development-mode answer (set AZURE_OPENAI_* environment variables for a real model).

Based on indexed excerpts, the United Kingdom (London) and Turkey (Ankara) are NATO members whose capitals exceed 5 million people.

Question: Which NATO member countries have capitals with more than 5 million people?
3 changes: 3 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/Expected/index.summary.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
chunkCount=13
source=national-capitals.pdf
source=nato-members.pdf
6 changes: 6 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/Expected/search.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query=capital cities with more than 50% of national population
1. national-capitals.pdf p3
2. national-capitals.pdf p2
3. national-capitals.pdf p6
4. national-capitals.pdf p4
5. national-capitals.pdf p4
3 changes: 3 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/Expected/summarize.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Development-mode summary (set AZURE_OPENAI_* for a real model).

The document lists capital cities with population figures. Several capitals exceed 5 million residents.
175 changes: 175 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System.Text;
using Microsoft.Extensions.AI;
using MuPDF.NET.Examples.Common;
using MuPDF.NET.PDF4LLM;
using MuPDF.NET.PDF4LLM.AI;
using MuPDF.NET.PDF4LLM.AI.Models;
using MuPDF.NET.PDF4LLM.AI.Options;

namespace MuPDF.NET.Examples.PDF4LLM.LoadAi;

internal static class Program
{
static void Main(string[] args)
{
ExampleArgs.Parse(args);
ConsoleEx.Title("MuPDF.NET.PDF4LLM / 09-LoadAi");
LoadAi().GetAwaiter().GetResult();
}

/// <summary>
/// Index PDFs for RAG, then Search / Ask / Summarize.
/// </summary>
static async Task LoadAi()
{
string capitals = ExamplePaths.Pdf4LlmInput("national-capitals.pdf");
string nato = ExamplePaths.Pdf4LlmInput("nato-members.pdf");
string output = ExamplePaths.Output("MuPDF.NET.PDF4LLM", "09-LoadAi", "load-ai.txt");
var check = new ResultCheck("MuPDF.NET.PDF4LLM", "09-LoadAi");

bool azure = TryAzureOptions(out MicrosoftAIConnectorOptions options);
if (!azure)
options = MicrosoftAIConnectorOptions.CreateForDevelopment(chatClient: new ExampleRagChatClient());

ConsoleEx.Info(azure
? "Pipeline: Azure OpenAI (live model — ask/search text not compared to Expected/)"
: "Pipeline: development (in-memory index + local IChatClient)");

bool prior = MuPDF4LLM.UseLayout;
try
{
// Classic extract so Expected/ stays stable without pymupdf-layout.
MuPDF4LLM.SetUseLayout(false);
options.UseLayout = false;

// PDF → chunks → embed → vector index (Microsoft.Extensions.AI).
AiDocumentCollection aiDocs = await MuPDF4LLM.LoadAiAsync(
new[] { capitals, nato },
options);

const string question =
"Which NATO member countries have capitals with more than 5 million people?";
const string searchQuery = "capital cities with more than 50% of national population";

string answer = await aiDocs.AskAsync(question);
string summary = await aiDocs.SummarizeAsync("national-capitals.pdf");
IReadOnlyList<SearchResult> hits = await aiDocs.SearchAsync(searchQuery, topK: 5);

var report = new StringBuilder();
report.Append("chunkCount=").Append(aiDocs.ChunkCount).Append('\n');
foreach (string source in aiDocs.SourceFiles.OrderBy(s => s, StringComparer.OrdinalIgnoreCase))
report.Append("source=").Append(source).Append('\n');

// Index shape is local extraction — comparable even with Azure embeddings.
check.Text(report.ToString(), "index.summary.txt");

var search = new StringBuilder();
search.Append("query=").Append(searchQuery).Append('\n');
for (int i = 0; i < hits.Count; i++)
{
SearchResult hit = hits[i];
search.Append(i + 1).Append(". ")
.Append(hit.Chunk.SourceFileName)
.Append(" p").Append(hit.Chunk.PageNumber)
.Append('\n');
}

File.WriteAllText(output, report.ToString()
+ "--- ask ---\n" + ResultCheck.NormalizeText(answer) + "\n"
+ "--- summarize ---\n" + ResultCheck.NormalizeText(summary) + "\n"
+ "--- search ---\n" + search);

ConsoleEx.Info($"Indexed {aiDocs.ChunkCount} chunks");
ConsoleEx.Info($"Ask: {answer.Replace('\n', ' ')[..Math.Min(120, answer.Length)]}");

if (!azure)
{
check.Text(ResultCheck.NormalizeText(answer) + "\n", "ask.txt");
check.Text(ResultCheck.NormalizeText(summary) + "\n", "summarize.txt");
check.Text(search.ToString(), "search.txt");
}
}
finally
{
MuPDF4LLM.SetUseLayout(prior);
}

check.Finish();
}

static bool TryAzureOptions(out MicrosoftAIConnectorOptions options)
{
string? endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
string? apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
string? chatDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_CHAT_DEPLOYMENT");
string? embeddingDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT");

if (string.IsNullOrWhiteSpace(endpoint)
|| string.IsNullOrWhiteSpace(apiKey)
|| string.IsNullOrWhiteSpace(chatDeployment)
|| string.IsNullOrWhiteSpace(embeddingDeployment))
{
options = null!;
return false;
}

options = new AzureAiOptions
{
OpenAiEndpoint = endpoint,
OpenAiApiKey = apiKey,
ChatDeploymentName = chatDeployment,
EmbeddingDeploymentName = embeddingDeployment,
SearchEndpoint = Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT"),
SearchApiKey = Environment.GetEnvironmentVariable("AZURE_SEARCH_API_KEY"),
SearchIndexName = Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX"),
}.ToConnectorOptions();
return true;
}

/// <summary>Offline chat client so the sample runs with no API keys.</summary>
sealed class ExampleRagChatClient : IChatClient
{
public ChatClientMetadata Metadata { get; } = new("example-rag");

public Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
var list = messages.ToList();
string system = list.FirstOrDefault(m => m.Role == ChatRole.System)?.Text ?? "";
string user = list.LastOrDefault(m => m.Role == ChatRole.User)?.Text ?? "";

string answer;
if (system.Contains("Summarize", StringComparison.OrdinalIgnoreCase))
{
answer =
"Development-mode summary (set AZURE_OPENAI_* for a real model).\n\n" +
"The document lists capital cities with population figures. " +
"Several capitals exceed 5 million residents.";
}
else
{
int idx = user.IndexOf("Question:", StringComparison.Ordinal);
string question = idx >= 0 ? user[(idx + "Question:".Length)..].Trim() : user;
answer =
"Development-mode answer (set AZURE_OPENAI_* environment variables for a real model).\n\n" +
"Based on indexed excerpts, the United Kingdom (London) and Turkey (Ankara) " +
"are NATO members whose capitals exceed 5 million people.\n\n" +
$"Question: {question}";
}

return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, answer)));
}

public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();

public object? GetService(Type serviceType, object? serviceKey = null) => null;

public void Dispose() { }
}
}
43 changes: 43 additions & 0 deletions MuPDF.NET.PDF4LLM/09-LoadAi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 09-LoadAi

Index PDFs into a RAG pipeline (`LoadAiAsync`), then **search**, **ask**, and **summarize**. This is why MuPDF.NET.PDF4LLM exists: extract clean page text, then hand it to `Microsoft.Extensions.AI` (`IEmbeddingGenerator` / `IChatClient`).

Default run needs **no API keys**. It uses `CreateForDevelopment()` (hash embeddings + in-memory index) and a local `IChatClient` so `dotnet run` and `run-all` always PASS.

If all of `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_CHAT_DEPLOYMENT`, and `AZURE_OPENAI_EMBEDDING_DEPLOYMENT` are set, the **same** `AskAsync` / `SearchAsync` calls use Azure OpenAI (optional Azure AI Search via `AZURE_SEARCH_*`). Live model text is written to `Output/` and is **not** compared to `Expected/`.

Swap the stub `IChatClient` for OpenAI, Azure, Ollama, or any `Microsoft.Extensions.AI` client. The MuPDF.NET.PDF4LLM part stays the same. Ready-made Ollama samples: [`10-Ollama`](../10-Ollama/) and [`11-OllamaMagazines`](../11-OllamaMagazines/).

## Sample method

`LoadAi()` in `Program.cs`.

## Package

- [MuPDF.NET.PDF4LLM](https://www.nuget.org/packages/MuPDF.NET.PDF4LLM)

## Input / output

| | Path |
|--|------|
| Input | `national-capitals.pdf`, `nato-members.pdf` |
| Output | `load-ai.txt` |
| Expected | `index.summary.txt` (+ `ask.txt` / `summarize.txt` / `search.txt` in development mode) |

## Run

```powershell
dotnet run --project MuPDF.NET.PDF4LLM\09-LoadAi
```

## APIs used

- `MuPDF4LLM.LoadAiAsync`
- `MicrosoftAIConnectorOptions.CreateForDevelopment` (or `AzureAiOptions.ToConnectorOptions`)
- `AiDocumentCollection.AskAsync` / `SummarizeAsync` / `SearchAsync`

## Related

- [`01-ToMarkdown`](../01-ToMarkdown/) — the Markdown this pipeline indexes
- [`06-LlamaMarkdownReader`](../06-LlamaMarkdownReader/) — LlamaIndex-shaped chunks without embed/ask
- [`10-Ollama`](../10-Ollama/) — same pipeline with a local Ollama chat + embedding model
13 changes: 13 additions & 0 deletions MuPDF.NET.PDF4LLM/10-Ollama/10-Ollama.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>MuPDF.NET.Examples.PDF4LLM.OllamaCapitals</RootNamespace>
<AssemblyName>10-Ollama</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MuPDF.NET.PDF4LLM" Version="$(ArtifexMuPDFNetPDF4LLMVersion)" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.10.0" />
<PackageReference Include="OllamaSharp" Version="5.4.30" />
<ProjectReference Include="..\..\Common\Examples.Common.csproj" />
</ItemGroup>
</Project>
Loading