diff --git a/Input/MuPDF.NET.PDF4LLM/Magazines-PDF.pdf b/Input/MuPDF.NET.PDF4LLM/Magazines-PDF.pdf
new file mode 100644
index 0000000..3104e1d
Binary files /dev/null and b/Input/MuPDF.NET.PDF4LLM/Magazines-PDF.pdf differ
diff --git a/Input/MuPDF.NET.PDF4LLM/nato-members.pdf b/Input/MuPDF.NET.PDF4LLM/nato-members.pdf
new file mode 100644
index 0000000..9c2f971
Binary files /dev/null and b/Input/MuPDF.NET.PDF4LLM/nato-members.pdf differ
diff --git a/MuPDF.NET.Examples.sln b/MuPDF.NET.Examples.sln
index aa3bd08..f1628ed 100644
--- a/MuPDF.NET.Examples.sln
+++ b/MuPDF.NET.Examples.sln
@@ -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
@@ -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
@@ -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
diff --git a/MuPDF.NET.PDF4LLM/01-ToMarkdown/README.md b/MuPDF.NET.PDF4LLM/01-ToMarkdown/README.md
index 6fc4f75..4775084 100644
--- a/MuPDF.NET.PDF4LLM/01-ToMarkdown/README.md
+++ b/MuPDF.NET.PDF4LLM/01-ToMarkdown/README.md
@@ -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)
diff --git a/MuPDF.NET.PDF4LLM/06-LlamaMarkdownReader/README.md b/MuPDF.NET.PDF4LLM/06-LlamaMarkdownReader/README.md
index 195e693..114a233 100644
--- a/MuPDF.NET.PDF4LLM/06-LlamaMarkdownReader/README.md
+++ b/MuPDF.NET.PDF4LLM/06-LlamaMarkdownReader/README.md
@@ -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
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/09-LoadAi.csproj b/MuPDF.NET.PDF4LLM/09-LoadAi/09-LoadAi.csproj
new file mode 100644
index 0000000..91455d5
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/09-LoadAi.csproj
@@ -0,0 +1,12 @@
+
+
+ Exe
+ MuPDF.NET.Examples.PDF4LLM.LoadAi
+ 09-LoadAi
+
+
+
+
+
+
+
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/ask.txt b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/ask.txt
new file mode 100644
index 0000000..a30b3e0
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/ask.txt
@@ -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?
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/index.summary.txt b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/index.summary.txt
new file mode 100644
index 0000000..f2dab46
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/index.summary.txt
@@ -0,0 +1,3 @@
+chunkCount=13
+source=national-capitals.pdf
+source=nato-members.pdf
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/search.txt b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/search.txt
new file mode 100644
index 0000000..2b9693f
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/search.txt
@@ -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
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/summarize.txt b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/summarize.txt
new file mode 100644
index 0000000..6be7cd4
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/Expected/summarize.txt
@@ -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.
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/Program.cs b/MuPDF.NET.PDF4LLM/09-LoadAi/Program.cs
new file mode 100644
index 0000000..bd62f35
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/Program.cs
@@ -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();
+ }
+
+ ///
+ /// Index PDFs for RAG, then Search / Ask / Summarize.
+ ///
+ 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 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;
+ }
+
+ /// Offline chat client so the sample runs with no API keys.
+ sealed class ExampleRagChatClient : IChatClient
+ {
+ public ChatClientMetadata Metadata { get; } = new("example-rag");
+
+ public Task GetResponseAsync(
+ IEnumerable 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 GetStreamingResponseAsync(
+ IEnumerable messages,
+ ChatOptions? options = null,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException();
+
+ public object? GetService(Type serviceType, object? serviceKey = null) => null;
+
+ public void Dispose() { }
+ }
+}
diff --git a/MuPDF.NET.PDF4LLM/09-LoadAi/README.md b/MuPDF.NET.PDF4LLM/09-LoadAi/README.md
new file mode 100644
index 0000000..42ff860
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/09-LoadAi/README.md
@@ -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
diff --git a/MuPDF.NET.PDF4LLM/10-Ollama/10-Ollama.csproj b/MuPDF.NET.PDF4LLM/10-Ollama/10-Ollama.csproj
new file mode 100644
index 0000000..1aef150
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/10-Ollama/10-Ollama.csproj
@@ -0,0 +1,13 @@
+
+
+ Exe
+ MuPDF.NET.Examples.PDF4LLM.OllamaCapitals
+ 10-Ollama
+
+
+
+
+
+
+
+
diff --git a/MuPDF.NET.PDF4LLM/10-Ollama/OllamaRag.cs b/MuPDF.NET.PDF4LLM/10-Ollama/OllamaRag.cs
new file mode 100644
index 0000000..6920af5
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/10-Ollama/OllamaRag.cs
@@ -0,0 +1,286 @@
+using System.Net.Http;
+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;
+using OllamaSharp;
+using OllamaSharp.Models;
+
+namespace MuPDF.NET.Examples.PDF4LLM.OllamaSupport;
+
+///
+/// Shared Ollama + LoadAiAsync host used by 10-Ollama and 11-OllamaMagazines.
+///
+internal static class OllamaRag
+{
+ public static async Task RunAsync(
+ string exampleName,
+ string pdfFileName,
+ IReadOnlyList askQuestions,
+ IReadOnlyList searchQueries,
+ bool repl)
+ {
+ string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") ?? "http://localhost:11434";
+ string chatModel = Environment.GetEnvironmentVariable("OLLAMA_MODEL") ?? "mistral";
+ string embedModel = Environment.GetEnvironmentVariable("OLLAMA_EMBED_MODEL") ?? "nomic-embed-text";
+
+ if (!Uri.TryCreate(host, UriKind.Absolute, out Uri? uri))
+ {
+ Console.WriteLine($"SKIP — Invalid OLLAMA_HOST '{host}'.");
+ return;
+ }
+
+ string pdfPath = ExamplePaths.Pdf4LlmInput(pdfFileName);
+ string output = ExamplePaths.Output("MuPDF.NET.PDF4LLM", exampleName, "ollama.txt");
+
+ using HttpClient chatHttp = CreateHttp(uri);
+ using HttpClient embedHttp = CreateHttp(uri);
+ using OllamaApiClient chatOllama = new(chatHttp, chatModel);
+ using OllamaApiClient embedOllama = new(embedHttp, embedModel);
+
+ string? resolvedChat = await TryResolveModelAsync(chatOllama, uri, chatModel);
+ if (resolvedChat == null)
+ return;
+
+ chatModel = resolvedChat;
+ string? resolvedEmbed = chatModel;
+ if (!string.Equals(chatModel, embedModel, StringComparison.OrdinalIgnoreCase))
+ {
+ resolvedEmbed = await TryResolveModelAsync(embedOllama, uri, embedModel);
+ if (resolvedEmbed == null)
+ return;
+ }
+
+ embedModel = resolvedEmbed!;
+ ConsoleEx.Info($"Ollama {uri} chat={chatModel} embed={embedModel}");
+ ConsoleEx.Info($"Indexing {pdfFileName} …");
+
+ bool layout = MuPDF4LLM.LayoutAvailable;
+ bool prior = MuPDF4LLM.UseLayout;
+ var report = new System.Text.StringBuilder();
+ try
+ {
+ MuPDF4LLM.SetUseLayout(layout);
+ var options = new MicrosoftAIConnectorOptions
+ {
+ ChatClient = chatOllama,
+ EmbeddingGenerator = embedOllama,
+ UseLayout = layout,
+ };
+
+ AiDocumentCollection aiDocs = await MuPDF4LLM.LoadAiAsync(new[] { pdfPath }, options);
+ ConsoleEx.Info($"Indexed {aiDocs.ChunkCount} chunks from: {string.Join(", ", aiDocs.SourceFiles)}");
+
+ report.Append("chunkCount=").Append(aiDocs.ChunkCount).Append('\n');
+ foreach (string source in aiDocs.SourceFiles)
+ report.Append("source=").Append(source).Append('\n');
+
+ Console.WriteLine();
+ Console.WriteLine("--- Ask ---");
+ foreach (string question in askQuestions)
+ {
+ string answer = await PrintAskAsync(aiDocs, question);
+ report.Append("--- ask: ").Append(question).Append(" ---\n");
+ report.Append(ResultCheck.NormalizeText(answer)).Append('\n');
+ }
+
+ Console.WriteLine("--- Search ---");
+ foreach (string query in searchQueries)
+ {
+ string search = await PrintSearchAsync(aiDocs, query);
+ report.Append("--- search: ").Append(query).Append(" ---\n");
+ report.Append(search);
+ }
+
+ File.WriteAllText(output, report.ToString());
+ ConsoleEx.Info($"Wrote: {output}");
+
+ var check = new ResultCheck("MuPDF.NET.PDF4LLM", exampleName);
+ check.Equal(aiDocs.ChunkCount > 0, true, "indexed chunks");
+ check.Finish();
+
+ if (!repl || Console.IsInputRedirected)
+ return;
+
+ Console.WriteLine($"Ask another question about {pdfFileName}. Type 'exit' to quit.");
+ Console.WriteLine();
+ while (true)
+ {
+ Console.Write("> ");
+ string? line = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(line))
+ continue;
+ if (line.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ break;
+ Console.WriteLine();
+ await PrintAskAsync(aiDocs, line);
+ }
+ }
+ catch (HttpRequestException ex)
+ {
+ Console.Error.WriteLine($"Indexing or chat failed: {ex.Message}");
+ if (ex.InnerException != null)
+ Console.Error.WriteLine(ex.InnerException.Message);
+ if (ex.StatusCode == System.Net.HttpStatusCode.NotImplemented)
+ {
+ Console.Error.WriteLine("That model cannot embed. Pull an embedding model:");
+ Console.Error.WriteLine(" ollama pull nomic-embed-text");
+ }
+
+ Environment.ExitCode = 1;
+ }
+ finally
+ {
+ MuPDF4LLM.SetUseLayout(prior);
+ }
+ }
+
+ static async Task PrintAskAsync(AiDocumentCollection aiDocs, string question)
+ {
+ Console.WriteLine($"Ask: {question}");
+ try
+ {
+ string answer = await aiDocs.AskAsync(question);
+ Console.WriteLine(answer);
+ Console.WriteLine();
+ return answer;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Ask failed: {ex.Message}");
+ if (ex.InnerException != null)
+ Console.Error.WriteLine(ex.InnerException.Message);
+ Console.WriteLine();
+ return "";
+ }
+ }
+
+ static async Task PrintSearchAsync(AiDocumentCollection aiDocs, string query)
+ {
+ Console.WriteLine($"Search: {query}");
+ var block = new System.Text.StringBuilder();
+ try
+ {
+ IReadOnlyList hits = await aiDocs.SearchAsync(query, topK: 10);
+ Console.WriteLine($" Ranked all {aiDocs.ChunkCount} chunks; showing top {hits.Count}.");
+ for (int i = 0; i < hits.Count; i++)
+ {
+ SearchResult hit = hits[i];
+ if (i > 0)
+ Console.WriteLine();
+ Console.WriteLine($" {i + 1}. [{hit.Score:F3}] {hit.Chunk.SourceFileName} p{hit.Chunk.PageNumber}");
+ foreach (string line in SplitLines(hit.Chunk.Text))
+ Console.WriteLine($" {line}");
+ block.Append(i + 1).Append(". ")
+ .Append(hit.Chunk.SourceFileName)
+ .Append(" p").Append(hit.Chunk.PageNumber)
+ .Append('\n');
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Search failed: {ex.Message}");
+ if (ex.InnerException != null)
+ Console.Error.WriteLine(ex.InnerException.Message);
+ }
+
+ Console.WriteLine();
+ return block.ToString();
+ }
+
+ static IEnumerable SplitLines(string text)
+ {
+ string[] lines = text.Replace("\r\n", "\n").Replace('\r', '\n').Trim().Split('\n');
+ for (int i = 0; i < lines.Length; i++)
+ lines[i] = lines[i].TrimEnd();
+ return lines;
+ }
+
+ static HttpClient CreateHttp(Uri uri) =>
+ new()
+ {
+ BaseAddress = uri,
+ Timeout = TimeSpan.FromMinutes(10),
+ };
+
+ static async Task TryResolveModelAsync(OllamaApiClient ollama, Uri uri, string model)
+ {
+ try
+ {
+ if (!await ollama.IsRunningAsync())
+ {
+ Console.WriteLine($"SKIP — Cannot reach Ollama at {uri}.");
+ Console.WriteLine("Install Ollama, start it (`ollama serve` or the Ollama app), then:");
+ Console.WriteLine(" ollama pull mistral");
+ Console.WriteLine(" ollama pull nomic-embed-text");
+ Console.WriteLine("See MuPDF.NET.PDF4LLM/10-Ollama/README.md.");
+ return null;
+ }
+
+ IEnumerable local = await ollama.ListLocalModelsAsync();
+ string? installed = ResolveInstalledName(local, model);
+ if (installed == null)
+ {
+ Console.WriteLine($"SKIP — Model '{model}' is not installed.");
+ Console.WriteLine($"Install it with: ollama pull {model}");
+ return null;
+ }
+
+ if (!string.Equals(installed, model, StringComparison.OrdinalIgnoreCase))
+ ConsoleEx.Info($"Using installed model '{installed}' for '{model}'.");
+
+ ollama.SelectedModel = installed;
+ return installed;
+ }
+ catch (HttpRequestException ex)
+ {
+ Console.WriteLine($"SKIP — Cannot reach Ollama at {uri}.");
+ Console.WriteLine(ex.Message);
+ Console.WriteLine("Start the Ollama app or `ollama serve`, then retry.");
+ return null;
+ }
+ }
+
+ static string? ResolveInstalledName(IEnumerable models, string requested)
+ {
+ var names = new List();
+ foreach (Model model in models)
+ {
+ AddName(names, model.Name);
+ AddName(names, model.ModelName);
+ }
+
+ foreach (string name in names)
+ {
+ if (string.Equals(name, requested, StringComparison.OrdinalIgnoreCase))
+ return name;
+ }
+
+ string latest = requested + ":latest";
+ foreach (string name in names)
+ {
+ if (string.Equals(name, latest, StringComparison.OrdinalIgnoreCase))
+ return name;
+ }
+
+ foreach (string name in names)
+ {
+ int colon = name.IndexOf(':');
+ string baseName = colon < 0 ? name : name.Substring(0, colon);
+ if (string.Equals(baseName, requested, StringComparison.OrdinalIgnoreCase))
+ return name;
+ }
+
+ return null;
+ }
+
+ static void AddName(List names, string? candidate)
+ {
+ if (string.IsNullOrEmpty(candidate))
+ return;
+ if (!names.Exists(n => string.Equals(n, candidate, StringComparison.OrdinalIgnoreCase)))
+ names.Add(candidate);
+ }
+}
diff --git a/MuPDF.NET.PDF4LLM/10-Ollama/Program.cs b/MuPDF.NET.PDF4LLM/10-Ollama/Program.cs
new file mode 100644
index 0000000..3e09296
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/10-Ollama/Program.cs
@@ -0,0 +1,48 @@
+using MuPDF.NET.Examples.Common;
+using MuPDF.NET.Examples.PDF4LLM.OllamaSupport;
+
+namespace MuPDF.NET.Examples.PDF4LLM.OllamaCapitals;
+
+internal static class Program
+{
+ static void Main(string[] args)
+ {
+ ExampleArgs.Parse(args);
+ ConsoleEx.Title("MuPDF.NET.PDF4LLM / 10-Ollama");
+ OllamaCapitals(args).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Index national-capitals.pdf with a local Ollama chat + embedding model, then Ask / Search.
+ ///
+ static Task OllamaCapitals(string[] args)
+ {
+ bool repl = args.Any(a => string.Equals(a, "--repl", StringComparison.OrdinalIgnoreCase));
+ return OllamaRag.RunAsync(
+ "10-Ollama",
+ "national-capitals.pdf",
+ AskQuestions,
+ SearchQueries,
+ repl);
+ }
+
+ static readonly string[] AskQuestions =
+ {
+ "What is the capital of South Sudan?",
+ "What was the population of Juba in 2017?",
+ "What is the capital of Canada?",
+ "What percentage of Gibraltar's population lives in the capital?",
+ "What is the capital of Tanzania?",
+ "What is the listed population of Vatican City?",
+ "What is the capital of the United States?",
+ "What percentage of the United States population lives in Washington D.C.?",
+ "What is the melting point of unobtainium?",
+ };
+
+ static readonly string[] SearchQueries =
+ {
+ "capital of South Sudan",
+ "Vatican City population",
+ "Ottawa Canada capital",
+ };
+}
diff --git a/MuPDF.NET.PDF4LLM/10-Ollama/README.md b/MuPDF.NET.PDF4LLM/10-Ollama/README.md
new file mode 100644
index 0000000..14a0eff
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/10-Ollama/README.md
@@ -0,0 +1,129 @@
+# 10-Ollama
+
+Index `national-capitals.pdf` with **[Ollama](https://ollama.com)** as the `IChatClient` and `IEmbeddingGenerator`, then **Ask** and **Search** through `MuPDF4LLM.LoadAiAsync`.
+
+This is the same RAG path as [`09-LoadAi`](../09-LoadAi/), but the chat and embedding models run **locally**. Default `dotnet run` / `run-all` **SKIP** when Ollama is not running or the models are not pulled — no API keys required.
+
+You need **two** Ollama models: a chat model (`mistral`) and a separate embedding model (`nomic-embed-text`). Chat models typically cannot embed.
+
+## Install Ollama
+
+### Windows
+
+1. Download the installer from [https://ollama.com/download](https://ollama.com/download) (or `winget install Ollama.Ollama`).
+2. Run the installer. The Ollama app starts a server at `http://localhost:11434`.
+3. Open a terminal and confirm:
+
+```powershell
+ollama --version
+curl http://localhost:11434/api/tags
+```
+
+If the app is not running, start it from the Start menu, or:
+
+```powershell
+ollama serve
+```
+
+### macOS
+
+```bash
+brew install ollama
+# or download https://ollama.com/download
+ollama serve
+```
+
+### Linux
+
+```bash
+curl -fsSL https://ollama.com/install.sh | sh
+ollama serve
+```
+
+The install script adds a systemd user service on many distros. `curl http://localhost:11434/api/tags` should return JSON.
+
+## Pull the models
+
+```powershell
+ollama pull mistral
+ollama pull nomic-embed-text
+```
+
+| Model | Role | Notes |
+|-------|------|--------|
+| `mistral` | Chat (`AskAsync`) | 8k context; override with `OLLAMA_MODEL` (`llama3.1`, `qwen3`, …) |
+| `nomic-embed-text` | Embeddings (index + search) | Required; `mistral` cannot embed |
+
+List what is installed:
+
+```powershell
+ollama list
+```
+
+## Sample method
+
+`OllamaCapitals()` in `Program.cs`. Shared host: `OllamaRag.RunAsync` in `OllamaRag.cs`.
+
+## Package
+
+- [MuPDF.NET.PDF4LLM](https://www.nuget.org/packages/MuPDF.NET.PDF4LLM)
+- [OllamaSharp](https://www.nuget.org/packages/OllamaSharp) (`IChatClient` / `IEmbeddingGenerator` for Ollama)
+- [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI)
+
+## Input / output
+
+| | Path |
+|--|------|
+| Input | `Input/MuPDF.NET.PDF4LLM/national-capitals.pdf` |
+| Output | `Output/MuPDF.NET.PDF4LLM/10-Ollama/ollama.txt` |
+| Expected | none (live model text is not baselined) |
+
+## Run
+
+From the **MuPDF.NET.Examples** repo root, after Ollama is up and the two models are pulled:
+
+```powershell
+dotnet run --project MuPDF.NET.PDF4LLM\10-Ollama
+```
+
+Interactive extra questions (after the canned Ask / Search list):
+
+```powershell
+dotnet run --project MuPDF.NET.PDF4LLM\10-Ollama -- --repl
+```
+
+Without Ollama, `dotnet run` prints `SKIP —`. `run-all` always skips this project (live model; run it directly).
+
+## Environment
+
+| Variable | Default | Meaning |
+|----------|---------|---------|
+| `OLLAMA_HOST` | `http://localhost:11434` | Ollama API base URL |
+| `OLLAMA_MODEL` | `mistral` | Chat model |
+| `OLLAMA_EMBED_MODEL` | `nomic-embed-text` | Embedding model |
+
+## Canned questions
+
+Facts from the capitals table (South Sudan / Juba, Canada, Gibraltar, Tanzania, Vatican City, United States) plus **What is the melting point of unobtainium?** which the PDF cannot answer (the model should abstain).
+
+## APIs used
+
+- `MuPDF4LLM.LoadAiAsync`
+- `MicrosoftAIConnectorOptions` (`ChatClient`, `EmbeddingGenerator`, `UseLayout`)
+- `AiDocumentCollection.AskAsync` / `SearchAsync`
+- `OllamaSharp.OllamaApiClient`
+
+## Troubleshooting
+
+| Symptom | What to do |
+|---------|------------|
+| `SKIP — Cannot reach Ollama` | Start the Ollama app or `ollama serve`. Check `OLLAMA_HOST`. |
+| `SKIP — Model 'mistral' is not installed` | `ollama pull mistral` (or your `OLLAMA_MODEL`) |
+| `That model cannot embed` / HTTP 501 | Pull `nomic-embed-text`. Do not use the chat model as the embedder. |
+| Indexing is slow or the connection resets | Local models are heavy; keep `nomic-embed-text` for embed and `mistral` for chat. Close other GPU apps. |
+| Answers ignore the PDF | Ask uses retrieved excerpts only. Try Search first to see which chunks ranked. |
+
+## Related
+
+- [`09-LoadAi`](../09-LoadAi/) — same `LoadAiAsync` path with a stub client or Azure OpenAI
+- [`11-OllamaMagazines`](../11-OllamaMagazines/) — same Ollama wiring on `Magazines-PDF.pdf`
diff --git a/MuPDF.NET.PDF4LLM/11-OllamaMagazines/11-OllamaMagazines.csproj b/MuPDF.NET.PDF4LLM/11-OllamaMagazines/11-OllamaMagazines.csproj
new file mode 100644
index 0000000..b8a5d53
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/11-OllamaMagazines/11-OllamaMagazines.csproj
@@ -0,0 +1,16 @@
+
+
+ Exe
+ MuPDF.NET.Examples.PDF4LLM.OllamaMagazines
+ 11-OllamaMagazines
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MuPDF.NET.PDF4LLM/11-OllamaMagazines/Program.cs b/MuPDF.NET.PDF4LLM/11-OllamaMagazines/Program.cs
new file mode 100644
index 0000000..5185622
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/11-OllamaMagazines/Program.cs
@@ -0,0 +1,49 @@
+using MuPDF.NET.Examples.Common;
+using MuPDF.NET.Examples.PDF4LLM.OllamaSupport;
+
+namespace MuPDF.NET.Examples.PDF4LLM.OllamaMagazines;
+
+internal static class Program
+{
+ static void Main(string[] args)
+ {
+ ExampleArgs.Parse(args);
+ ConsoleEx.Title("MuPDF.NET.PDF4LLM / 11-OllamaMagazines");
+ OllamaMagazines(args).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Index Magazines-PDF.pdf with a local Ollama chat + embedding model, then Ask / Search.
+ ///
+ static Task OllamaMagazines(string[] args)
+ {
+ bool repl = args.Any(a => string.Equals(a, "--repl", StringComparison.OrdinalIgnoreCase));
+ return OllamaRag.RunAsync(
+ "11-OllamaMagazines",
+ "Magazines-PDF.pdf",
+ AskQuestions,
+ SearchQueries,
+ repl);
+ }
+
+ static readonly string[] AskQuestions =
+ {
+ "What is a magazine, according to the definition in this document?",
+ "Name the ten types of magazines listed in the document.",
+ "What do fashion magazines focus on?",
+ "Which magazine is given as an example of a lifestyle magazine?",
+ "What details does a magazine masthead contain?",
+ "What is the difference between a magazine and a newspaper?",
+ "What is the difference between a magazine, a journal, and a catalog?",
+ "What standard magazine sizes are used in the US and UK?",
+ "What type of paper is commonly used for magazine printing?",
+ "What is the melting point of unobtainium?",
+ };
+
+ static readonly string[] SearchQueries =
+ {
+ "lifestyle magazine Rolling Stone",
+ "magazine masthead editors writers designers",
+ "standard magazine sizes 8.5 by 11",
+ };
+}
diff --git a/MuPDF.NET.PDF4LLM/11-OllamaMagazines/README.md b/MuPDF.NET.PDF4LLM/11-OllamaMagazines/README.md
new file mode 100644
index 0000000..9d5d6c0
--- /dev/null
+++ b/MuPDF.NET.PDF4LLM/11-OllamaMagazines/README.md
@@ -0,0 +1,50 @@
+# 11-OllamaMagazines
+
+Index `Magazines-PDF.pdf` (a primer on magazine types, uses, parts, and sizes) with **local Ollama**, then **Ask** and **Search**.
+
+Install Ollama and pull models first — see **[`10-Ollama/README.md`](../10-Ollama/README.md)** (Windows / macOS / Linux install, `ollama pull mistral`, `ollama pull nomic-embed-text`).
+
+## Sample method
+
+`OllamaMagazines()` in `Program.cs`. Shared host: [`OllamaRag.cs`](../10-Ollama/OllamaRag.cs).
+
+## Package
+
+- [MuPDF.NET.PDF4LLM](https://www.nuget.org/packages/MuPDF.NET.PDF4LLM)
+- [OllamaSharp](https://www.nuget.org/packages/OllamaSharp)
+- [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI)
+
+## Input / output
+
+| | Path |
+|--|------|
+| Input | `Input/MuPDF.NET.PDF4LLM/Magazines-PDF.pdf` |
+| Output | `Output/MuPDF.NET.PDF4LLM/11-OllamaMagazines/ollama.txt` |
+| Expected | none (live model text is not baselined) |
+
+## Run
+
+```powershell
+dotnet run --project MuPDF.NET.PDF4LLM\11-OllamaMagazines
+dotnet run --project MuPDF.NET.PDF4LLM\11-OllamaMagazines -- --repl
+```
+
+Without Ollama, `dotnet run` prints `SKIP —`. `run-all` always skips this project (live model; run it directly).
+
+Same environment variables as [`10-Ollama`](../10-Ollama/): `OLLAMA_HOST`, `OLLAMA_MODEL`, `OLLAMA_EMBED_MODEL`.
+
+## Canned questions
+
+Definition of a magazine; the ten types; fashion magazines; Rolling Stone as a lifestyle example; masthead contents; magazine vs newspaper; magazine vs journal vs catalog; US/UK sizes (8.5"×11" and 5.5"×8.5"); pulp paper; plus **What is the melting point of unobtainium?** (should abstain).
+
+## APIs used
+
+- `MuPDF4LLM.LoadAiAsync`
+- `MicrosoftAIConnectorOptions` (`ChatClient`, `EmbeddingGenerator`, `UseLayout`)
+- `AiDocumentCollection.AskAsync` / `SearchAsync`
+- `OllamaSharp.OllamaApiClient`
+
+## Related
+
+- [`10-Ollama`](../10-Ollama/) — Ollama install + capitals PDF
+- [`09-LoadAi`](../09-LoadAi/) — `LoadAiAsync` without Ollama
diff --git a/README.md b/README.md
index e6e3907..8e6fa8b 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,8 @@ Package versions are pinned in [`Versions.props`](Versions.props). [`NuGet.Confi
- Packages from [nuget.org](https://www.nuget.org/) (see `Versions.props`)
- Optional `MUPDF_OFFICE_KEY` for Office unlock
- `02-ToJsonLayout` / `03-ToText`: optional [pymupdf-layout](https://pypi.org/project/pymupdf-layout/) for layout mode
+- `09-LoadAi`: optional `AZURE_OPENAI_*` (and `AZURE_SEARCH_*`) for a live model; default run needs no keys
+- `10-Ollama` / `11-OllamaMagazines`: local [Ollama](https://ollama.com) (`ollama pull mistral` and `ollama pull nomic-embed-text`); SKIP when Ollama is not running — see `MuPDF.NET.PDF4LLM/10-Ollama/README.md`
## Run (compare against Expected/)
@@ -96,7 +98,7 @@ Each project folder has its own `README.md` (what it shows, inputs, how to run,
| Product | Projects |
|---------|----------|
| MuPDF.NET | Open/Save, Pages, Render, Parallel pixmap, Text, Recolor, Story/HTML, Annotations, Widgets, Insert/Replace/Extract images, Outline/Links, Tables, Barcodes, Embedded files, Metadata, TextWriter, Draw shapes, ZUGFeRD, Color management, Rewrite images, GetDrawings, Rotate/Crop, Encrypt, Compress, Watermark, Image→PDF, Image filters, Page ops, File annot |
-| MuPDF.NET.PDF4LLM | Markdown, JSON layout, Plain text, OCR, Tables→CSV, Llama markdown reader, GetKeyValues, Markdown→PDF |
+| MuPDF.NET.PDF4LLM | Markdown, JSON layout, Plain text, OCR, Tables→CSV, Llama markdown reader, GetKeyValues, Markdown→PDF, LoadAi (RAG ask/search), Ollama capitals, Ollama magazines |
| MuPDF.NET.Office | Unlock/fonts, Open HWPX/DOCX, Export PDF, Export MD/JSON, With PDF4LLM |
## Versions
diff --git a/Versions.props b/Versions.props
index 9a125e7..810f0d9 100644
--- a/Versions.props
+++ b/Versions.props
@@ -5,7 +5,7 @@
-->
3.28.2.4
- 1.28.2
+ 1.28.2.1
1.28.2
diff --git a/run-all.ps1 b/run-all.ps1
index c5eaf7f..0b189a5 100644
--- a/run-all.ps1
+++ b/run-all.ps1
@@ -32,6 +32,14 @@ $pass = 0
foreach ($p in $projects) {
$name = $p.Directory.Name
+ # Live Ollama samples: SKIP in the batch runner (slow, needs a local server).
+ # Run them directly: dotnet run --project MuPDF.NET.PDF4LLM\10-Ollama
+ if ($name -eq '10-Ollama' -or $name -eq '11-OllamaMagazines') {
+ Write-Host "SKIP $name (live Ollama; see that project's README)"
+ $skip++
+ continue
+ }
+
$argList = @(
'run', '--project', $p.FullName,
'-c', $Configuration, '--no-build'